@felloinnovations/brand-kit 0.5.0 → 0.5.3

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.
Files changed (124) hide show
  1. package/AGENTS.md +29 -9
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +14 -6
  4. package/design-system/01-how-claude-decides.md +5 -3
  5. package/design-system/03-colour.md +22 -3
  6. package/design-system/05-space-and-layout.md +2 -1
  7. package/design-system/08-choosing-components.md +20 -4
  8. package/design-system/09-charts.md +7 -4
  9. package/design-system/10-patterns.md +1 -1
  10. package/design-system/11-page-recipes.md +40 -1
  11. package/design-system/13-motion.md +4 -4
  12. package/design-system/15-ai-experiences.md +11 -0
  13. package/design-system/17-decisions.md +51 -0
  14. package/design-system/components/AppShell/README.md +89 -21
  15. package/design-system/components/AppShell/preview.html +148 -3
  16. package/design-system/components/AppShell/style.css +47 -0
  17. package/design-system/components/Avatar/README.md +6 -3
  18. package/design-system/components/Avatar/preview.html +2 -1
  19. package/design-system/components/Avatar/style.css +3 -0
  20. package/design-system/components/Banner/README.md +9 -5
  21. package/design-system/components/Banner/preview.html +9 -0
  22. package/design-system/components/Banner/style.css +3 -0
  23. package/design-system/components/Button/README.md +29 -2
  24. package/design-system/components/Button/preview.html +33 -1
  25. package/design-system/components/Button/style.css +21 -0
  26. package/design-system/components/Calendar/README.md +148 -0
  27. package/design-system/components/Calendar/preview.html +449 -0
  28. package/design-system/components/Calendar/style.css +61 -0
  29. package/design-system/components/Card/README.md +13 -0
  30. package/design-system/components/Card/preview.html +23 -0
  31. package/design-system/components/Card/style.css +19 -0
  32. package/design-system/components/ChatCard/README.md +74 -3
  33. package/design-system/components/ChatCard/preview.html +126 -25
  34. package/design-system/components/ChatCard/style.css +6 -0
  35. package/design-system/components/CheckRow/README.md +1 -1
  36. package/design-system/components/CheckRow/preview.html +1 -1
  37. package/design-system/components/CheckRow/style.css +1 -1
  38. package/design-system/components/Chip/README.md +16 -7
  39. package/design-system/components/Chip/preview.html +25 -1
  40. package/design-system/components/Chip/style.css +5 -0
  41. package/design-system/components/Combobox/README.md +10 -4
  42. package/design-system/components/Combobox/preview.html +19 -4
  43. package/design-system/components/Combobox/style.css +7 -1
  44. package/design-system/components/CommentThread/README.md +138 -0
  45. package/design-system/components/CommentThread/preview.html +134 -0
  46. package/design-system/components/CommentThread/style.css +41 -0
  47. package/design-system/components/DateInput/README.md +38 -4
  48. package/design-system/components/DateInput/preview.html +50 -2
  49. package/design-system/components/DateInput/style.css +6 -0
  50. package/design-system/components/Dialog/README.md +26 -3
  51. package/design-system/components/Dialog/preview.html +80 -0
  52. package/design-system/components/Dialog/style.css +28 -3
  53. package/design-system/components/EmptyState/README.md +1 -1
  54. package/design-system/components/Fold/README.md +21 -0
  55. package/design-system/components/Fold/preview.html +44 -1
  56. package/design-system/components/Fold/style.css +30 -0
  57. package/design-system/components/InlineEdit/README.md +116 -0
  58. package/design-system/components/InlineEdit/preview.html +135 -0
  59. package/design-system/components/InlineEdit/style.css +34 -0
  60. package/design-system/components/Input/README.md +20 -1
  61. package/design-system/components/Input/preview.html +24 -0
  62. package/design-system/components/Input/style.css +13 -1
  63. package/design-system/components/KanbanBoard/README.md +69 -5
  64. package/design-system/components/KanbanBoard/preview.html +129 -0
  65. package/design-system/components/KanbanBoard/style.css +32 -1
  66. package/design-system/components/ListDetail/preview.html +1 -1
  67. package/design-system/components/Matrix/README.md +1 -1
  68. package/design-system/components/Matrix/preview.html +1 -1
  69. package/design-system/components/Motion/README.md +2 -2
  70. package/design-system/components/Motion/preview.html +8 -8
  71. package/design-system/components/PlanTimeline/README.md +144 -0
  72. package/design-system/components/PlanTimeline/preview.html +109 -0
  73. package/design-system/components/PlanTimeline/style.css +64 -0
  74. package/design-system/components/README.md +12 -7
  75. package/design-system/components/ScoreHero/README.md +2 -2
  76. package/design-system/components/ScoreHero/style.css +1 -1
  77. package/design-system/components/ShareBar/README.md +1 -1
  78. package/design-system/components/ShareBar/style.css +1 -1
  79. package/design-system/components/Skeleton/README.md +82 -17
  80. package/design-system/components/Skeleton/preview.html +84 -2
  81. package/design-system/components/Skeleton/style.css +9 -14
  82. package/design-system/components/Starter/README.md +1 -1
  83. package/design-system/components/Starter/preview.html +1 -1
  84. package/design-system/components/Stepper/README.md +81 -5
  85. package/design-system/components/Stepper/preview.html +64 -1
  86. package/design-system/components/Stepper/style.css +29 -0
  87. package/design-system/components/StreakCalendar/README.md +1 -0
  88. package/design-system/components/Table/README.md +8 -4
  89. package/design-system/components/Table/preview.html +16 -1
  90. package/design-system/components/Table/style.css +3 -1
  91. package/design-system/components/TextDiff/README.md +96 -0
  92. package/design-system/components/TextDiff/preview.html +42 -0
  93. package/design-system/components/TextDiff/style.css +21 -0
  94. package/design-system/components/TickStrip/README.md +1 -1
  95. package/design-system/components/TickStrip/preview.html +1 -1
  96. package/design-system/components/TickStrip/style.css +1 -1
  97. package/design-system/components/Timeline/README.md +4 -2
  98. package/design-system/components/TrendLine/README.md +38 -7
  99. package/design-system/components/TrendLine/preview.html +140 -3
  100. package/design-system/components/TrendLine/style.css +12 -2
  101. package/design-system/components/Well/README.md +1 -1
  102. package/design-system/components/Well/style.css +2 -2
  103. package/design-system/components/_house.css +1 -1
  104. package/design-system/components/bundle.css +501 -30
  105. package/design-system/tokens.json +17 -7
  106. package/dist/fello-core.css +2 -2
  107. package/dist/fello.css +2 -2
  108. package/dist/fello.scoped.css +2 -2
  109. package/dist/index.d.ts +1 -1
  110. package/dist/scoped.js +2 -2
  111. package/dist/tokens.d.ts +16 -8
  112. package/dist/tokens.js +16 -8
  113. package/package.json +7 -2
  114. package/scripts/check-drift.mjs +325 -0
  115. package/scripts/token-model.mjs +2 -1
  116. package/tokens/_dark.css +3 -2
  117. package/tokens/_product.css +13 -11
  118. package/tokens/palettes.ts +1 -1
  119. package/tokens/shadcn-compat.css +1 -1
  120. package/tokens/shadcn-compat.hsl.css +1 -1
  121. package/tokens/tokens.css +23 -15
  122. package/tokens/tokens.md +56 -50
  123. package/tokens/tokens.root.css +23 -15
  124. package/tokens/tokens.ts +17 -9
@@ -494,3 +494,54 @@ the team builds on the kit gets them, not North alone.
494
494
  | 5 Nov 2026 | Amit (via Claude, open to veto) | `--ws-duration-fill-page` (1700 ms, North's "Metric + goal bars fill") is a token: the tiles' charts and the goal bars both use it (D4). A tile's foot figure rises in after the draw; it does not count up (the 4 Nov rule: a headline figure shows its value) | tokens, Motion |
495
495
  | 5 Nov 2026 | Amit (via Claude, open to veto) | `TrendLine`: North's revenue chart and its NRR line become one kit chart, a value over periods on an axis as a line (dots, the hatched area under it) or as columns, with the Bars / Line morph (`--ws-spring-morph-chart`), the hovered column band and the hovered dot. Overturns 09-charts' "a free line chart: make it thin bars" for one series on an axis; two series stay two cards. Its columns start at zero (North's ARR chart starts at $20M: the kit does not) | Charts, `TrendLine`, `TrendBars`, `StepLine` |
496
496
 
497
+ ## Rulings added 10 Nov 2026 (kit 0.5.1)
498
+
499
+ Amit looked at the loading skeletons in an app built on the kit: the shimmer swept salmon, purple and green tints across
500
+ every grey ghost block. He wants loaders calm and neutral.
501
+
502
+ | Date | By | Decision | Where it bites |
503
+ | --- | --- | --- | --- |
504
+ | 10 Nov 2026 | Amit (via Claude) | Loading is one quiet tone: the shimmer (`--ws-shimmer`) is the ghost grey with a lighter pass of the same grey, never a chart hue, in light, night, the navy ground and every palette. Overturns D7's "loading is a shimmer in the chart colours", the Motion row "The skeleton breathes" (4 Nov) for its colours, and the loading shimmer as an exception to "no gradient text" in Colour. North's timing stays: the 300 % band at 100 deg over 1.8 s (`--ws-loop-shimmer` on `--ws-ease-shimmer`), still under reduced motion | Skeleton, Motion, Colour, Patterns, every `ws-shimmer` ghost |
505
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | How it is drawn. The ground is the ghost grey itself (`--ws-shimmer-ground` is now `--ws-ghost`: #E3DFDB, the night's #273544), the same at rest and in motion, so a ghost shows on the cream (1.29:1) and on the tray (1.13:1) in every theme and Skeleton's own ground for a card's foot is gone. The pass is the ghost 70 % of the way to the cream in light (#F6F3F0: 1.07:1 off the cream, 1.06:1 off the tray, so it shows on both) and 7 % toward the ink in dark (#354250; #364351 on the navy ground), the same step by eye (CIEDE2000 4.3); it derives from the ghost, the page and the ink, so a palette moves it. The chart ghost's colour band is dropped, not made neutral: in blend mode colour a neutral band has no hue and paints nothing, and in normal blend it would paint the card between the bars. Each bar is now a ghost under the same shimmer, so one token drives every loader | tokens (`_product.css`, `_dark.css`), Skeleton, the Motion card (its two rows now Close and Not yet) |
506
+ | 10 Nov 2026 | Open, for Amit to rule | Not changed: `StatusLine`'s working words still pass through the AA steps of salmon, purple and teal. They are words at work, not a ghost; if "loaders" covers them too, they take a neutral pass next | StatusLine |
507
+
508
+ ## Rulings added 10 Nov 2026 (Hemanth's feedback, kit 0.5.2)
509
+
510
+ Hemanth built the Customer Value app on the kit (10 Nov call with Amit) and found what it was missing: two lines at
511
+ once on one chart (the kit said "two lines are two cards"), a whole page's loader, a stage track and a milestone row
512
+ for an onboarding, one card for two boards, a chat page for Calvin, and a way to update without re-running adopt,
513
+ which had stopped after 10 to 15 files. The look stays locked at 0.5 (Seth, Nikhil and Amit): everything here adds,
514
+ nothing restyles.
515
+
516
+ | Date | By | Decision | Where it bites |
517
+ | --- | --- | --- | --- |
518
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | `TrendLine` draws up to three series in one unit on one axis (`__series`), navy, teal, purple, with a key; a hover reads every series for the period. Several series are lines only: no hatch, no Bars / Line switch. Different units, or one series more than four times another, are two TrendLines stacked in one card on the same periods; never two value axes. Overturns the 5 Nov "two series stay two cards" | Charts, `TrendLine` |
519
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | A whole page loading is the page's own bar and board, not a new shape: the `PageBar` with its name at once, its data-bound words and act as ghosts, and the board holding tile and card skeletons where the real ones land (no new CSS). A list or a board page uses `Table`'s and `KanbanBoard`'s own ghosts | `Skeleton`, `EmptyState` |
520
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | A thing's stages read across (`Stepper --across`), the in-hand stage opening in view; dated points are a milestone row (`--milestones`), the measure on each milestone, one quiet act on one not yet reached. A stall or a late milestone is words in needs-you (`__late`); the ring stays navy. `Timeline` stays the feed of what happened | `Stepper`, `Timeline`, Choosing components |
521
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | Every board wears one card, its slots in a fixed order (kind and figure, time in stage, the title as the card's link, meta, up to three chips, a foot with people, a tally and one icon act, one well act when the stage waits on the person). Never a rail per column, a stage hue, an underlined title or a navy button on a card: a board is many cards, and one navy per page | `KanbanBoard` |
522
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | An email, a call and a text are `ChatCard` kinds (the sender's words quoted on a sunk plate, three lines at most; Reply, or Listen for a call), and an account is a kind with its standing as a rating chip and one navy act. An agent that works a book of accounts keeps one thread per account and a daily briefing on top, the one time it speaks first (recipe 25) | `ChatCard`, Page recipes, AI experiences |
523
+ | 10 Nov 2026 | Amit (via Claude) | An app already on the kit takes a newer release with `brand-catchup` (only what the releases changed), never a second adopt. Adopt is done when `check-drift` finds nothing left or every leftover is named, and it leaves a baseline in CI so nothing new drifts in | `brand-catchup`, `brand-adopt`, `brand-audit`, `check-drift`, AGENTS.md |
524
+
525
+ ## Rulings added 11 Nov 2026 (Madan's Marketing OS feedback and Hemanth's sidebar, kit 0.5.3)
526
+
527
+ Madan filed eighteen gaps from moving Marketing OS (ContentOS) onto the kit, and Hemanth asked for a sidebar that holds
528
+ sections. Nikhil (10 Nov): Marketing OS shows the new UI to Stephen before Thursday, and apps never build a missing
529
+ component with Claude, so the kit carries each of these. Everything adds; nothing that exists changes how it looks.
530
+
531
+ | Date | By | Decision | Where it bites |
532
+ | --- | --- | --- | --- |
533
+ | 10 Nov 2026 | Amit (via Claude, open to veto) | AppShell's sidebar holds sections: a page's own pages fold in place, one level only. Folded, a section opens a Menu panel beside the rail, and the person at the foot opens the account menu. A group stays always open | `AppShell`, Choosing components |
534
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A chosen option is a tick at the row's end, never the sunk cream (that is the keyboard's) | `Combobox` |
535
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A far month or year is two small native selects in the picker's head; Today and Clear sit in a foot under Menu's line | `DateInput` |
536
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A time, and a date with a time, are the native field in DateInput's shell; no kit picker draws hours | `DateInput`, Choosing components |
537
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A joined act's caret holds the act's other forms, never other acts; a navy joined act is the surface's one navy act | `Button` |
538
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | Chip `--sm` (20) and Avatar `--xs` (20) are the dense steps; a chip that acts keeps 24 | `Chip`, `Avatar` |
539
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | Error, warning, success and info map onto the kit's tones (alert, needs, positive, working): no separate status colours, no solid status fills, and a field's error stays needs. `--ws-tint-alert`, the alert red at 8 %, is the ground of an error status | Colour, `Banner`, `Chip`, tokens |
540
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A drawer rising from a pane casts `--ws-shadow-rise`, the menu's drop upward | Space and layout, tokens |
541
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A dialog a library mounts is `ws-dialog--mounted` with `ws-dialog__scrim`: placed by the kit, the same moves as keyframes, out on `data-state="closed"` or `data-leaving` | `Dialog`, Motion |
542
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A card fold's acts sit beside the summary, never inside it; `ws-fold__peek` shows only while folded; tones are Banner's three | `Fold` |
543
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A picture rounds its own corners and the card never clips; a grid choice is a flush ring plus a tick, never the tab | `Card`, Choosing components |
544
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | An InlineEdit has the same box and type at rest and editing; its focus ring is drawn inside the box; the pencil always shows on touch | `InlineEdit` |
545
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A mention is mid blue words with no fill; only a mention of you takes the needs tint. The comment marker on content is the blue card tint, never an underline | `CommentThread` |
546
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | A diff never says it with colour alone: a sign, and an underline for what was added; removed lines are not struck, so the old words stay readable | `TextDiff` |
547
+ | 11 Nov 2026 | Amit (via Claude, open to veto) | Things on dates are a `Calendar`; a plan across its window is a `PlanTimeline`. Today is the one salmon mark in both; tones follow the palette; neither restyles `Stepper`, which keeps steps and milestones without a date axis | `Calendar`, `PlanTimeline`, Choosing components |
@@ -5,7 +5,8 @@ nav to a drawer.
5
5
  ## Use it when
6
6
 
7
7
  - The product's work happens on pages a person moves between: a pipeline, a report, a list, settings.
8
- - There are more destinations than fit a flat list, and they fall into a few groups (Reports, Settings).
8
+ - There are more destinations than fit a flat list, and they fall into a few groups (Reports, Settings), or a page has
9
+ a few pages of its own (Onboarding: Board, Stalled, Graduated) that fold under it as a section.
9
10
  - Every page needs the same place for its name, its acts and a way to fold the nav away.
10
11
  - A team member needs to see who they are signed in as, pinned at the foot of the nav.
11
12
 
@@ -33,11 +34,24 @@ nav to a drawer.
33
34
  - Rows (`__rows`, `__row`): 28 tall (`--ws-control-h-sm`, 40 on touch), 2 apart, 10 px sides, 6 px corners. A 20 px
34
35
  icon box (the 16 px icon inside it), 4 px, then the name (`__name`) in 13 / 18 medium body grey, truncated with
35
36
  its full words in `title`. Rows are real links (`<a href>`).
37
+ - Section (`__section`): a row that folds its pages open in place (North's "Sidebar sections open / close"). A
38
+ `<details>` whose `<summary>` wears `__row`: the icon, the name, a 16 px chevron at the end (`__chevron`, in the
39
+ resting mark grey) that turns down as it opens. Its pages are a `__rows` list one level in (the icon box and its
40
+ gap, 24), so their words start on the section's words, 46 from the nav's side; they need no icon of their own.
41
+ One level only: a section never holds a section.
42
+ - Folded section (`__fly` and `__panel`): in the 64 px rail a section cannot open, so the `<details>` leaves and a
43
+ `<button>` row (`__row __fly`, the section's icon, its name in `title`) takes its place, opening the section's
44
+ pages as a `Menu` panel beside the rail (`ws-menu __panel`, a native popover): its top on the button's, 8 past the
45
+ rail's edge, flipped up when the room below runs out. The panel names the section in its eyebrow (`__eyebrow`, on
46
+ the words a `ws-menu-item` row starts on), then its pages as `ws-menu-item` links. Both are in the markup; the nav
47
+ shows one or the other.
36
48
  - Count (`__count`): Seth's navy pill, 18 tall, at least 18 wide, 11 px semibold figures in the act fill's word
37
49
  colour (`--ws-on-act`).
38
50
  - The person (`__foot`): pinned at the foot under a full-width 1 px rule, never scrolled away, 12 above and 6 below,
39
51
  on the rows' edges (22 in at the left, 18 at the right: the groups' 12 / 8 and a row's 10). A 28 px avatar, 10 px,
40
- then the name 13 / 16 medium navy (`__foot-name`) over the role in `ws-muted`.
52
+ then the name 13 / 16 medium navy (`__foot-name`) over the role in `ws-muted`. When the person opens their
53
+ account menu (a profile, "View as", sign out), the avatar and words sit in a button (`__me`) that opens a `Menu`:
54
+ a row's 10 px sides and 6 px corners and its wash, the foot's height and the avatar's 22 px edge unchanged.
41
55
  - **Stage** (`__stage`): `--ws-glass` with a 4 px blur (`--ws-glass-blur`), a 1 px `--ws-edge-stage` edge, 16 px corners
42
56
  (`--ws-radius-panel`), 13 from the window's top (`--ws-stage-inset-top`), 12 from its right and bottom
43
57
  (`--ws-stage-inset`), no gap to the nav (the nav's own 8 px is the gap). `--wash` gives it the Fia frames' cream
@@ -62,7 +76,8 @@ nav to a drawer.
62
76
  the folded rail).
63
77
  - **Touch**: the search box is 40 tall and its words 16, so a phone or tablet never zooms into it on focus.
64
78
  - In fello.css: `.ws-app-shell` (+ `--folded`, `--wash`), `__nav`, `__head`, `__wordmark`, `__groups`, `__group`,
65
- `__eyebrow`, `__rows`, `__row` (+ `--current`), `__name`, `__count`, `__foot`, `__foot-words`, `__foot-name`,
79
+ `__eyebrow`, `__rows`, `__row` (+ `--current`), `__name`, `__count`, `__section`, `__chevron`, `__fly`, `__panel`,
80
+ `__foot`, `__me`, `__foot-words`, `__foot-name`,
66
81
  `__stage`, `__bar` (+ `--scrolled`), `__title`, `__acts`, `__search`, `__menu`, `__body` (+ `--wide`); the 0.4
67
82
  rail markup `__rail` and `__mark` (+ `--current`, `--wordmark`) still draws, and so does a 0.4 nav (wordmark, group
68
83
  names, rows and person straight in `__nav`, no `__head` or `__groups`): the nav itself scrolls then, with the
@@ -73,7 +88,7 @@ nav to a drawer.
73
88
  | Variant | Width | Shows |
74
89
  | --- | --- | --- |
75
90
  | Open (default) | 232 px | The head, grouped rows with names and counts, the person |
76
- | Folded (`--folded`) | 64 px | The same nav: 40 px rows of icons, the current one on its pill; names faded (still read aloud), counts gone, each row's name and count in its `title` (a pointer's tooltip, a screen reader's description), group names and rules gone, the avatar centred |
91
+ | Folded (`--folded`) | 64 px | The same nav: 40 px rows of icons, the current one on its pill; names faded (still read aloud), counts gone, each row's name and count in its `title` (a pointer's tooltip, a screen reader's description), group names and rules gone, the avatar centred; each section a button that opens its pages in a panel beside the rail |
77
92
  | Phone (under 60rem of the shell) | none | The bar's menu button opens the same nav in a drawer at the drawer's width; the stage goes edge to edge, 16 px sides |
78
93
 
79
94
  | Body | Content width | Use |
@@ -95,6 +110,13 @@ nav to a drawer.
95
110
  - **Needs you**: the navy count pill. A bare number, its meaning in `.ws-sr` words inside it ("7 waiting"), never
96
111
  `aria-label` on the span (ARIA does not let a plain span carry a label).
97
112
  - **Long name**: truncates on one line, the full name in `title`.
113
+ - **Section open / closed**: the chevron points right when closed and down when open, turning on the fold spring as
114
+ the pages fold in (the house's fold on every kit `<details>`). Whether a section starts open is the app's: open the
115
+ one that holds the current page.
116
+ - **Closed on the current page**: a closed section that holds the current page wears the current pill itself (the
117
+ white pill, its icon salmon, its words at salmon's AA step), so the page is never lost behind a fold; opened, the
118
+ page takes the pill back. Folded, the section's button does the same, and the panel marks the page as the current
119
+ menu row.
98
120
  - **Folded**: only the icons show, so every row carries its name (and its count, "Pipeline, 7 waiting") in `title`;
99
121
  in React, a `Tooltip` on hover and focus.
100
122
  - **Many rows**: the groups scroll; the head and the person stay. In 0.4 markup (no `__groups`) the nav scrolls and the
@@ -113,7 +135,11 @@ nav to a drawer.
113
135
  - Keyboard: rows are links in document order; the fold button carries `aria-expanded` and toggles `--folded` on the
114
136
  shell. The phone's drawer is a `Dialog` (`ws-dialog--drawer`) holding the same `__nav` markup; the head's button
115
137
  becomes its close.
116
- - Pointer: hover only tints. Nothing opens on hover.
138
+ - Sections: the `<summary>` is the disclosure (Enter or Space opens and closes it, its open state read aloud). Folded,
139
+ the section's button opens its panel (Enter or Space, or a click; the browser tells a screen reader it is expanded),
140
+ Tab goes on into the panel's pages, and Escape or a click away closes it, the focus back on the button. Arrow keys
141
+ between the panel's rows are the app's, as on every `Menu`.
142
+ - Pointer: hover only tints. Nothing opens on hover, a folded section's panel included.
117
143
  - Motion (North): the fold trades width over `--ws-duration-shift` on `--ws-ease-shift`; the rows, the current pill
118
144
  and the fold button ride the nav's edge as it moves (nothing jumps to the middle and back), names fade over
119
145
  `--ws-duration-hover`, washes ease in over 200 ms, the bar's edge over `--ws-duration-settle`. The current pill
@@ -137,7 +163,10 @@ nav to a drawer.
137
163
 
138
164
  - A group's name says what kind of page it holds ("Reports"), sentence case, two words at most. The first group can
139
165
  go without one.
140
- - A row's name is the page's own name, sentence case.
166
+ - A row's name is the page's own name, sentence case. A section's name is the page its pages belong to
167
+ ("Onboarding"), and its pages say only what they add ("Board", "Stalled"), never the section's name again.
168
+ - A way out of the app (a full-screen app inside another: "Back to Turbo") is the last group's one row, a back chevron
169
+ for its icon, above the person; never in the foot and never at the top.
141
170
  - The bar names the page once, exactly as its row does; no second heading repeats it in the body.
142
171
  - No em dashes; a count is a bare number.
143
172
 
@@ -146,9 +175,9 @@ nav to a drawer.
146
175
  | Name | Type | Meaning |
147
176
  | --- | --- | --- |
148
177
  | `groups` | `{ name?: string; items: NavRow[] }[]` | The nav's groups, in order |
149
- | `NavRow` | `{ label, icon, href, current?, count? }` | One row: its words, its 16 px icon, whether it is the current page, its count |
150
- | `person` | `{ name, role, initials }` | The foot |
151
- | `folded` | `boolean` | The 64 px rail |
178
+ | `NavRow` | `{ label, icon, href, current?, count?, items?: NavRow[] }` | One row: its words, its 16 px icon, whether it is the current page, its count; with `items` it is a section (its own `href` unused), one level only |
179
+ | `person` | `{ name, role, initials, menu?: ReactNode }` | The foot; with `menu`, the person is a button that opens it |
180
+ | `folded` | `boolean` | The 64 px rail (the app keeps the person's choice, in storage, and sets `--folded`) |
152
181
  | `bar` | `ReactNode` | The bar's content: a title, a `Breadcrumb` or a `PageBar`, and its acts |
153
182
  | `wash` | `boolean` | The cream stage instead of glass |
154
183
 
@@ -157,15 +186,20 @@ nav to a drawer.
157
186
  - **One navy button per page**: the bar's primary is the page's one; nothing in the body adds a second.
158
187
  - **The body is the page**: it brings the 32 px gutter; a page inside never adds its own padding or a `.ws-page`.
159
188
  - **The person never scrolls away** and the head never moves: only the groups scroll (K11).
160
- - **Real links, `aria-current="page"`**: never a clickable `<li>`, never colour alone.
189
+ - **Real links, `aria-current="page"`**: never a clickable `<li>`, never colour alone. A section is a `<summary>` (and,
190
+ folded, a `<button>`), never a link that also folds.
191
+ - **Sections or groups, by what they hold**: a group names a kind of page ("Reports") and is always open; a section is
192
+ one page's own pages and folds. A group can hold sections; a section never holds a section.
161
193
  - **Seth's tokens only**: no raw colour, size or corner; dark comes from the tokens (D12).
162
194
 
163
195
  ## Source
164
196
 
165
197
  **Derived from**: Seth's North app (`style1.css` 22 to 112, 531 to 560; the 232 px sidebar, its 28 px rows, rules,
166
- navy count, white current pill, the 64 px rail, the glass stage and the frame), the Fia frames' 56 px thread header and
167
- 32 px gutter (D4), and Revenue OS faults K1, K2, K3, K11 and K12. **Precedent**: kit 0.4's AppShell, upgraded in place
168
- 4 Nov 2026 (the 0.4 markup still draws).
198
+ navy count, white current pill, the 64 px rail, the glass stage and the frame; "Sidebar sections open / close" on the
199
+ fold spring), the Fia frames' 56 px thread header and 32 px gutter (D4), and Revenue OS faults K1, K2, K3, K11 and K12.
200
+ Sections, the folded section's panel, the person's menu and the way out come from Customer Value's own rail
201
+ (`CvRail`, Hemanth, 10 Nov 2026), so the app can drop it. **Precedent**: kit 0.4's AppShell, upgraded in place 4 Nov
202
+ 2026 (the 0.4 markup still draws).
169
203
 
170
204
  ## Markup
171
205
 
@@ -195,15 +229,48 @@ navy count, white current pill, the 64 px rail, the glass stage and the frame),
195
229
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13V8M8 13V3M13 13V9.5"/></svg>
196
230
  <span class="ws-app-shell__name">Performance</span>
197
231
  </a></li>
232
+ <!-- A section: its pages fold open in place; folded, the button opens them beside the rail -->
233
+ <li>
234
+ <details class="ws-app-shell__section" open>
235
+ <summary class="ws-app-shell__row">
236
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M2 6.5h12"/></svg>
237
+ <span class="ws-app-shell__name">Onboarding</span>
238
+ <svg class="ws-app-shell__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 5.2L9.4 8l-2.8 2.8"/></svg>
239
+ </summary>
240
+ <ul class="ws-app-shell__rows">
241
+ <li><a href="/onboarding" class="ws-app-shell__row"><span class="ws-app-shell__name">Board</span></a></li>
242
+ <li><a href="/onboarding/stalled" class="ws-app-shell__row"><span class="ws-app-shell__name">Stalled</span></a></li>
243
+ </ul>
244
+ </details>
245
+ <button type="button" class="ws-app-shell__row ws-app-shell__fly" popovertarget="nav-onboarding" title="Onboarding">
246
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M2 6.5h12"/></svg>
247
+ <span class="ws-app-shell__name">Onboarding</span>
248
+ </button>
249
+ <div id="nav-onboarding" class="ws-menu ws-app-shell__panel" popover aria-label="Onboarding">
250
+ <p class="ws-app-shell__eyebrow">Onboarding</p>
251
+ <ul>
252
+ <li><a href="/onboarding" class="ws-menu-item">Board</a></li>
253
+ <li><a href="/onboarding/stalled" class="ws-menu-item">Stalled</a></li>
254
+ </ul>
255
+ </div>
256
+ </li>
198
257
  </ul>
199
258
  </div>
200
259
  </div>
201
260
  <div class="ws-app-shell__foot">
202
- <span class="ws-avatar ws-avatar--sm ws-avatar--plain">PN</span>
203
- <span class="ws-app-shell__foot-words">
204
- <span class="ws-app-shell__foot-name">Priya Nair</span>
205
- <span class="ws-muted">Team lead</span>
206
- </span>
261
+ <!-- A static person is the avatar and words straight in the foot; with an account menu, a button: -->
262
+ <button type="button" class="ws-app-shell__me" popovertarget="nav-me" aria-haspopup="menu">
263
+ <span class="ws-avatar ws-avatar--sm ws-avatar--plain">PN</span>
264
+ <span class="ws-app-shell__foot-words">
265
+ <span class="ws-app-shell__foot-name">Priya Nair</span>
266
+ <span class="ws-muted">Team lead</span>
267
+ </span>
268
+ </button>
269
+ <div id="nav-me" class="ws-menu" popover role="menu" aria-label="Your account">
270
+ <a href="/profile" class="ws-menu-item" role="menuitem">Your profile</a>
271
+ <div class="ws-menu-line" role="separator"></div>
272
+ <button type="button" class="ws-menu-item" role="menuitem">Sign out</button>
273
+ </div>
207
274
  </div>
208
275
  </nav>
209
276
  <div class="ws-app-shell__stage">
@@ -229,12 +296,13 @@ navy count, white current pill, the 64 px rail, the glass stage and the frame),
229
296
 
230
297
  ## Styles
231
298
 
232
- `AppShell/style.css`, with `IconButton`, `Button` and `Avatar` for the bar's acts and the person (`ws-muted` is the
233
- house's). Tokens: `--ws-frame`,
299
+ `AppShell/style.css`, with `IconButton`, `Button` and `Avatar` for the bar's acts and the person, and `Menu` for a
300
+ folded section's panel and the person's menu (`ws-muted` is the house's). Tokens: `--ws-frame`,
234
301
  `--ws-glass`, `--ws-glass-blur`, `--ws-stage-wash`, `--ws-edge-stage`, `--ws-stage-inset-top`, `--ws-stage-inset`,
235
302
  `--ws-sidebar-width`, `--ws-header-h`, `--ws-control-h-sm`, `--ws-touch`, `--ws-field`, `--ws-field-ink`,
236
303
  `--ws-focus-width`, `--ws-focus-color`, `--ws-focus-offset`, `--ws-radius-row`, `--ws-radius-control`,
237
304
  `--ws-radius-panel`, `--ws-hover`, `--ws-hover-strong`, `--ws-float-rim`, `--ws-shade`, `--ws-ink`,
238
305
  `--color-surface`, `--color-text-brand-strong`, `--ws-chart-under-half`, `--ws-on-act`, `--ws-edge-card`,
239
- `--ws-hairline`, `--ws-chart-card-w`, `--ws-gap-component`, `--ws-gap-group`, `--ws-text-value`, `--ws-duration-shift`, `--ws-ease-shift`. The shell sets `--ws-gutter` to `--space-xl` (32)
306
+ `--ws-hairline`, `--ws-chart-card-w`, `--ws-gap-component`, `--ws-gap-group`, `--ws-text-value`, `--ws-duration-shift`, `--ws-ease-shift`, `--ws-mark-muted`, `--ws-icon-lg`, `--ws-spring-fold`,
307
+ `--ws-spring-fold-duration`. The shell sets `--ws-gutter` to `--space-xl` (32)
240
308
  for everything inside it.
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Navigation" height=2372 -->
1
+ <!-- @dsCard group="Navigation" height=3552 -->
2
2
  <!doctype html>
3
3
  <html>
4
4
  <head>
@@ -116,6 +116,148 @@
116
116
  </div></div>
117
117
  </div>
118
118
 
119
+ <div class="ws-stack">
120
+ <span class="ws-muted">Sections: a row that folds its pages open in place, the chevron turning, the pages one level in. A closed section holding the current page wears its pill. A way out of the app is the last group&rsquo;s row; the person opens the account menu</span>
121
+ <div style="width: 1240px; max-width: 100%; height: 640px; border-radius: var(--ws-radius-panel); overflow: hidden;"><div class="ws-app-shell">
122
+ <nav class="ws-app-shell__nav" aria-label="Main">
123
+ <div class="ws-app-shell__head">
124
+ <span class="ws-app-shell__wordmark">Customer Value</span>
125
+ <button type="button" class="ws-icon-btn" aria-label="Fold the menu" aria-expanded="true">
126
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2.5" width="12" height="11" rx="2"/><path d="M6 2.5v11"/></svg>
127
+ </button>
128
+ </div>
129
+ <div class="ws-app-shell__groups">
130
+ <div class="ws-app-shell__group">
131
+ <ul class="ws-app-shell__rows">
132
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5 8 3l5 4.5V13H3Z"/></svg><span class="ws-app-shell__name">Home</span></a></li>
133
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h10M3 8h10M3 12h6"/></svg><span class="ws-app-shell__name">Calvin</span><span class="ws-app-shell__count">2<span class="ws-sr"> waiting</span></span></a></li>
134
+ </ul>
135
+ </div>
136
+ <div class="ws-app-shell__group">
137
+ <h2 class="ws-app-shell__eyebrow">Book</h2>
138
+ <ul class="ws-app-shell__rows">
139
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="5.6" r="2.4"/><path d="M3 13c0-2.5 2.2-4 5-4s5 1.5 5 4"/></svg><span class="ws-app-shell__name">Accounts</span></a></li>
140
+ <li>
141
+ <details class="ws-app-shell__section" open>
142
+ <summary class="ws-app-shell__row"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M2 6.5h12"/></svg><span class="ws-app-shell__name">Onboarding</span><svg class="ws-app-shell__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 5.2L9.4 8l-2.8 2.8"/></svg></summary>
143
+ <ul class="ws-app-shell__rows">
144
+ <li><a href="#" class="ws-app-shell__row" aria-current="page" onclick="return false;"><span class="ws-app-shell__name">Board</span></a></li>
145
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><span class="ws-app-shell__name">Stalled</span><span class="ws-app-shell__count">12<span class="ws-sr"> stalled</span></span></a></li>
146
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><span class="ws-app-shell__name">Graduated</span></a></li>
147
+ </ul>
148
+ </details>
149
+ </li>
150
+ <li>
151
+ <details class="ws-app-shell__section">
152
+ <summary class="ws-app-shell__row"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5.5h10v7H3Z"/><path d="M5.5 5.5V3.5h5v2"/></svg><span class="ws-app-shell__name">Tickets</span><svg class="ws-app-shell__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 5.2L9.4 8l-2.8 2.8"/></svg></summary>
153
+ <ul class="ws-app-shell__rows">
154
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><span class="ws-app-shell__name">Open</span></a></li>
155
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><span class="ws-app-shell__name">Waiting on the customer</span></a></li>
156
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><span class="ws-app-shell__name">Closed</span></a></li>
157
+ </ul>
158
+ </details>
159
+ </li>
160
+ </ul>
161
+ </div>
162
+ <div class="ws-app-shell__group">
163
+ <h2 class="ws-app-shell__eyebrow">Team</h2>
164
+ <ul class="ws-app-shell__rows">
165
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13V8M8 13V3M13 13V9.5"/></svg><span class="ws-app-shell__name">Performance</span></a></li>
166
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><circle cx="11.5" cy="7" r="1.8"/><path d="M1.5 13c0-2.2 2-3.5 4.5-3.5s4.5 1.3 4.5 3.5M10.5 10c1.8 0 3.5.9 3.5 2.6"/></svg><span class="ws-app-shell__name">My team</span></a></li>
167
+ </ul>
168
+ </div>
169
+ <div class="ws-app-shell__group">
170
+ <ul class="ws-app-shell__rows">
171
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 4 5.5 8l4 4"/></svg><span class="ws-app-shell__name">Back to Turbo</span></a></li>
172
+ </ul>
173
+ </div>
174
+ </div>
175
+ <div class="ws-app-shell__foot">
176
+ <button type="button" class="ws-app-shell__me" popovertarget="shell-me" aria-haspopup="menu">
177
+ <span class="ws-avatar ws-avatar--sm ws-avatar--plain">HD</span>
178
+ <span class="ws-app-shell__foot-words">
179
+ <span class="ws-app-shell__foot-name">Halli Doig</span>
180
+ <span class="ws-muted">Value delivery lead</span>
181
+ </span>
182
+ </button>
183
+ <div id="shell-me" class="ws-menu" popover role="menu" aria-label="Your account">
184
+ <a href="#" class="ws-menu-item" role="menuitem" onclick="return false;">Your profile</a>
185
+ <a href="#" class="ws-menu-item" role="menuitem" onclick="return false;">View as someone else</a>
186
+ <div class="ws-menu-line" role="separator"></div>
187
+ <button type="button" class="ws-menu-item" role="menuitem" popovertarget="shell-me" popovertargetaction="hide">Sign out</button>
188
+ </div>
189
+ </div>
190
+ </nav>
191
+ <div class="ws-app-shell__stage">
192
+ <header class="ws-app-shell__bar">
193
+ <button type="button" class="ws-icon-btn ws-app-shell__menu" aria-label="Open the menu">
194
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg>
195
+ </button>
196
+ <h1 class="ws-app-shell__title">Onboarding</h1>
197
+ </header>
198
+ <main class="ws-app-shell__body">
199
+ <section class="ws-card ws-card--inset ws-stack" aria-labelledby="shell-s1"><h2 class="ws-title" id="shell-s1">Twelve onboardings have stalled</h2><p class="ws-body">The oldest has waited 54 days for its first meeting.</p></section>
200
+ </main>
201
+ </div>
202
+ </div></div>
203
+ </div>
204
+
205
+ <div class="ws-stack">
206
+ <span class="ws-muted">Folded, a section&rsquo;s row opens its pages as a menu panel beside the rail: it names the section, the current page marked, Escape or a click away closes it and the focus goes back to the row. The row stands in for the current page it holds</span>
207
+ <div style="width: 1040px; max-width: 100%; height: 460px; border-radius: var(--ws-radius-panel); overflow: hidden;"><div class="ws-app-shell ws-app-shell--folded">
208
+ <nav class="ws-app-shell__nav" aria-label="Main">
209
+ <div class="ws-app-shell__head">
210
+ <span class="ws-app-shell__wordmark">Customer Value</span>
211
+ <button type="button" class="ws-icon-btn" aria-label="Open the menu" aria-expanded="false">
212
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2.5" width="12" height="11" rx="2"/><path d="M6 2.5v11"/></svg>
213
+ </button>
214
+ </div>
215
+ <div class="ws-app-shell__groups">
216
+ <div class="ws-app-shell__group">
217
+ <ul class="ws-app-shell__rows">
218
+ <li><a href="#" class="ws-app-shell__row" title="Home" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5 8 3l5 4.5V13H3Z"/></svg><span class="ws-app-shell__name">Home</span></a></li>
219
+ <li><a href="#" class="ws-app-shell__row" title="Accounts" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="5.6" r="2.4"/><path d="M3 13c0-2.5 2.2-4 5-4s5 1.5 5 4"/></svg><span class="ws-app-shell__name">Accounts</span></a></li>
220
+ <li>
221
+ <details class="ws-app-shell__section">
222
+ <summary class="ws-app-shell__row"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M2 6.5h12"/></svg><span class="ws-app-shell__name">Onboarding</span><svg class="ws-app-shell__chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 5.2L9.4 8l-2.8 2.8"/></svg></summary>
223
+ <ul class="ws-app-shell__rows">
224
+ <li><a href="#" class="ws-app-shell__row" aria-current="page" onclick="return false;"><span class="ws-app-shell__name">Board</span></a></li>
225
+ <li><a href="#" class="ws-app-shell__row" onclick="return false;"><span class="ws-app-shell__name">Stalled</span></a></li>
226
+ </ul>
227
+ </details>
228
+ <button type="button" class="ws-app-shell__row ws-app-shell__fly" popovertarget="shell-fly-onboarding" title="Onboarding"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="10" rx="1.5"/><path d="M2 6.5h12"/></svg><span class="ws-app-shell__name">Onboarding</span></button>
229
+ <div id="shell-fly-onboarding" class="ws-menu ws-app-shell__panel" popover aria-label="Onboarding">
230
+ <p class="ws-app-shell__eyebrow">Onboarding</p>
231
+ <ul>
232
+ <li><a href="#" class="ws-menu-item" aria-current="page" onclick="return false;">Board</a></li>
233
+ <li><a href="#" class="ws-menu-item" onclick="return false;">Stalled</a></li>
234
+ <li><a href="#" class="ws-menu-item" onclick="return false;">Graduated</a></li>
235
+ </ul>
236
+ </div>
237
+ </li>
238
+ <li><a href="#" class="ws-app-shell__row" title="Tickets" onclick="return false;"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5.5h10v7H3Z"/><path d="M5.5 5.5V3.5h5v2"/></svg><span class="ws-app-shell__name">Tickets</span></a></li>
239
+ </ul>
240
+ </div>
241
+ </div>
242
+ <div class="ws-app-shell__foot">
243
+ <span class="ws-avatar ws-avatar--sm ws-avatar--plain">HD</span>
244
+ <span class="ws-app-shell__foot-words"><span class="ws-app-shell__foot-name">Halli Doig</span><span class="ws-muted">Value delivery lead</span></span>
245
+ </div>
246
+ </nav>
247
+ <div class="ws-app-shell__stage">
248
+ <header class="ws-app-shell__bar">
249
+ <button type="button" class="ws-icon-btn ws-app-shell__menu" aria-label="Open the menu">
250
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg>
251
+ </button>
252
+ <h1 class="ws-app-shell__title">Onboarding board</h1>
253
+ </header>
254
+ <main class="ws-app-shell__body">
255
+ <section class="ws-card ws-card--inset ws-stack" aria-labelledby="shell-f1"><h2 class="ws-title" id="shell-f1">Twelve onboardings have stalled</h2><p class="ws-body">The oldest has waited 54 days for its first meeting.</p></section>
256
+ </main>
257
+ </div>
258
+ </div></div>
259
+ </div>
260
+
119
261
  <div class="ws-stack">
120
262
  <span class="ws-muted">Folded to the 64 px rail (the same nav, names faded but read aloud and in each row&rsquo;s title), the Fia frames&rsquo; cream stage (--wash), the thread header as the bar with its edge once scrolled, the body loading</span>
121
263
  <div style="width: 1040px; max-width: 100%; height: 420px; border-radius: var(--ws-radius-panel); overflow: hidden;"><div class="ws-app-shell ws-app-shell--folded ws-app-shell--wash">
@@ -369,10 +511,13 @@
369
511
  <script>
370
512
  /* The demo's wiring: a click makes the row the current page (an app's router does this; the pill slides on its own). */
371
513
  document.addEventListener('click', (e) => {
372
- const row = e.target.closest('.ws-app-shell__row');
514
+ const row = e.target.closest('a.ws-app-shell__row');
373
515
  if (!row) return;
374
- for (const r of row.closest('.ws-app-shell__nav').querySelectorAll('.ws-app-shell__row')) r === row ? r.setAttribute('aria-current', 'page') : r.removeAttribute('aria-current');
516
+ for (const r of row.closest('.ws-app-shell__nav').querySelectorAll('a.ws-app-shell__row')) r === row ? r.setAttribute('aria-current', 'page') : r.removeAttribute('aria-current');
375
517
  });
518
+ /* The demo opens the folded section's panel so it shows at rest (in an app the row's click opens it). */
519
+ const fly = document.querySelector('.ws-app-shell__fly');
520
+ try { document.getElementById('shell-fly-onboarding').showPopover({ source: fly }); } catch {}
376
521
  </script>
377
522
  </body>
378
523
  </html>
@@ -37,6 +37,35 @@
37
37
  /* A row's count: Seth's navy pill, 18 tall, its figure on the act fill's word colour. */
38
38
  .ws-app-shell__count { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; min-width: calc(var(--space-md) + var(--space-3xs)); height: calc(var(--space-md) + var(--space-3xs)); padding: 0 var(--space-xxs); border-radius: var(--radius-full); background: var(--color-text-heading); color: var(--ws-on-act); font-size: var(--ws-text-micro); line-height: 1; font-weight: 600; font-variant-numeric: tabular-nums; }
39
39
 
40
+ /* A section: a row that folds its pages open in place (North "Sidebar sections open / close", the house's fold on every
41
+ kit <details>). The row is the <summary>, wearing __row, its chevron at the end turning down as it opens; its pages sit
42
+ one level in, the icon box and its gap (24), so their words start on the section's words. A closed section that holds
43
+ the current page wears the current pill itself, so the page is never lost behind a fold. */
44
+ .ws-app-shell__section > summary { list-style: none; }
45
+ .ws-app-shell__section > summary::-webkit-details-marker { display: none; }
46
+ .ws-app-shell__section > .ws-app-shell__rows { margin-inline-start: calc(var(--ws-icon-lg) + var(--space-xxs)); padding-block-start: var(--space-3xs); }
47
+ .ws-app-shell__row > .ws-app-shell__chevron { color: var(--ws-mark-muted); }
48
+ .ws-app-shell__section[open] > summary > .ws-app-shell__chevron { transform: rotate(90deg); }
49
+ .ws-app-shell__section:not([open]):has(.ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"])) > summary { border-radius: var(--ws-radius-control); background: var(--color-surface); box-shadow: var(--ws-float-rim), 0 var(--ws-line) var(--ws-ring) color-mix(in srgb, var(--ws-shade) 6%, transparent), 0 0 0 var(--ws-hairline) color-mix(in srgb, var(--ws-shade) 4%, transparent); color: var(--color-text-brand-strong); }
50
+ .ws-app-shell__section:not([open]):has(.ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"])) > summary > svg:first-child { color: var(--ws-chart-under-half); }
51
+ /* Folded, a section cannot open in 64 px: its row (__fly, a button) opens its pages as a menu panel beside the rail
52
+ (__panel, a native popover: the top layer, light dismiss, Escape back to the button). The <details> leaves the rail.
53
+ The panel names the section (its eyebrow, which the rail hides) over its pages as menu rows. */
54
+ .ws-app-shell__fly { display: none; }
55
+ .ws-app-shell--folded .ws-app-shell__section { display: none; }
56
+ .ws-app-shell--folded .ws-app-shell__fly { display: flex; }
57
+ .ws-app-shell--folded .ws-app-shell__fly:has(+ .ws-app-shell__panel :is([aria-current="page"], .ws-menu-item--current)) { border-radius: var(--ws-radius-control); background: var(--color-surface); box-shadow: var(--ws-float-rim), 0 var(--ws-line) var(--ws-ring) color-mix(in srgb, var(--ws-shade) 6%, transparent), 0 0 0 var(--ws-hairline) color-mix(in srgb, var(--ws-shade) 4%, transparent); color: var(--color-text-brand-strong); }
58
+ .ws-app-shell--folded .ws-app-shell__fly:has(+ .ws-app-shell__panel :is([aria-current="page"], .ws-menu-item--current)) > svg { color: var(--ws-chart-under-half); }
59
+ .ws-app-shell--folded .ws-app-shell__panel .ws-app-shell__eyebrow { display: block; }
60
+ .ws-app-shell__panel > .ws-app-shell__eyebrow { margin: 0; }
61
+ .ws-app-shell__panel > ul { display: flex; flex-direction: column; gap: var(--space-3xs); }
62
+ /* Beside the rail, its top on the button's, 8 past the rail's edge (the button sits 12 inside it); it flips up when the
63
+ room below runs out. It grows from that corner. */
64
+ .ws-app-shell__panel { --ws-menu-from: 0px; transform-origin: top left; }
65
+ @supports (position-area: block-end) {
66
+ .ws-app-shell__panel.ws-menu[popover] { margin: 0 var(--space-xs) 0 calc(var(--space-sm) + var(--space-xs)); position-area: inline-end span-block-end; justify-self: auto; position-try-fallbacks: flip-block; }
67
+ }
68
+
40
69
  /* The person, pinned at the foot under a full-width rule. */
41
70
  .ws-app-shell__foot { flex-shrink: 0; display: flex; align-items: center; gap: var(--space-xs-plus); margin-top: auto; padding: var(--space-sm) var(--space-xs) var(--space-xxs-plus) var(--space-sm); border-top: var(--ws-line) solid var(--ws-edge-card); }
42
71
  .ws-app-shell__foot > :first-child { margin-inline-start: var(--space-xs-plus); }
@@ -48,6 +77,14 @@
48
77
  .ws-app-shell__nav:not(:has(> .ws-app-shell__groups)) > :is(.ws-app-shell__wordmark, .ws-app-shell__foot) { position: sticky; z-index: 1; background: var(--ws-frame); background-attachment: fixed; }
49
78
  .ws-app-shell__nav:not(:has(> .ws-app-shell__groups)) > .ws-app-shell__wordmark { top: 0; min-height: calc(var(--ws-header-h) + var(--ws-line)); margin-inline: 0; padding: var(--ws-line) var(--space-xs) 0 calc(var(--space-sm) + var(--space-xs-plus)); }
50
79
  .ws-app-shell__nav:not(:has(> .ws-app-shell__groups)) > .ws-app-shell__foot { bottom: 0; padding-bottom: var(--space-sm-plus); }
80
+ /* The person as a button (__me) that opens the account menu (Menu, above it once it flips): the foot keeps its height and
81
+ edges, the button takes a row's 10 sides and 6 px corners, so the avatar stays 22 in and the wash reads as a row's. */
82
+ .ws-app-shell__me { display: flex; flex: 1; align-items: center; gap: var(--space-xs-plus); min-width: 0; margin-block: calc(var(--space-3xs) * -1); padding: var(--space-3xs) var(--space-xs-plus); border: 0; border-radius: var(--ws-radius-row); background: none; font: inherit; color: inherit; text-align: start; cursor: pointer; }
83
+ .ws-app-shell__foot > .ws-app-shell__me { margin-inline: 0; }
84
+ .ws-app-shell__me:focus-visible { outline-offset: calc(var(--ws-focus-width) * -1); }
85
+ @media (hover: hover) { .ws-app-shell__me:hover { background: var(--ws-hover); } }
86
+ .ws-app-shell__me:active { background: var(--ws-hover-strong); }
87
+ .ws-app-shell--folded .ws-app-shell__me { flex: 0 0 auto; gap: 0; }
51
88
  .ws-app-shell__foot-words { display: flex; flex-direction: column; min-width: 0; }
52
89
  .ws-app-shell__foot-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--ws-text-control); line-height: var(--space-md); font-weight: 500; color: var(--color-text-heading); }
53
90
 
@@ -137,6 +174,9 @@
137
174
  :is(.ws-app-shell__groups, .ws-app-shell__nav:not(:has(> .ws-app-shell__groups))) { position: relative; anchor-scope: --ws-app-shell-current; }
138
175
  :is(.ws-app-shell__groups, .ws-app-shell__nav:not(:has(> .ws-app-shell__groups)))::before { content: ""; position: absolute; position-anchor: --ws-app-shell-current; inset: anchor(top) anchor(right) anchor(bottom) anchor(left); border-radius: var(--ws-radius-control); background: var(--color-surface); box-shadow: var(--ws-float-rim), 0 var(--ws-line) var(--ws-ring) color-mix(in srgb, var(--ws-shade) 6%, transparent), 0 0 0 var(--ws-hairline) color-mix(in srgb, var(--ws-shade) 4%, transparent); opacity: 0; pointer-events: none; }
139
176
  .ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"]) { anchor-name: --ws-app-shell-current; }
177
+ /* A closed section, or a folded section's button, stands in for the current page it holds (the page itself has no box). */
178
+ .ws-app-shell__section:not([open]):has(.ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"])) > summary,
179
+ .ws-app-shell--folded .ws-app-shell__fly:has(+ .ws-app-shell__panel :is([aria-current="page"], .ws-menu-item--current)) { anchor-name: --ws-app-shell-current; }
140
180
  .ws-app-shell__eyebrow { position: relative; } /* a group's name stays over the pill as it passes, as the rows do */
141
181
  }
142
182
 
@@ -145,10 +185,17 @@
145
185
  .ws-app-shell > .ws-app-shell__nav { transition: width var(--ws-duration-shift) var(--ws-ease-shift); }
146
186
  .ws-app-shell :is(.ws-app-shell__wordmark, .ws-app-shell__name, .ws-app-shell__foot-words) { transition: opacity var(--ws-duration-hover) var(--ws-ease-soft); }
147
187
  .ws-app-shell__row { transition: background-color var(--ws-duration-hover) var(--ws-ease-soft), color var(--ws-duration-hover) var(--ws-ease-soft); }
188
+ .ws-app-shell__me { transition: background-color var(--ws-duration-hover) var(--ws-ease-soft); }
189
+ .ws-app-shell__chevron { transition: transform var(--ws-spring-fold-duration) var(--ws-spring-fold); }
190
+ @supports (position-area: block-end) {
191
+ .ws-app-shell__panel.ws-menu[popover]:not(:popover-open) { margin-block-start: 0; }
192
+ @starting-style { .ws-app-shell__panel.ws-menu[popover]:popover-open { margin-block-start: 0; } }
193
+ }
148
194
  .ws-app-shell__bar { transition: box-shadow var(--ws-duration-settle) var(--ws-ease-soft); }
149
195
  @supports (anchor-name: --a) {
150
196
  :is(.ws-app-shell__groups, .ws-app-shell__nav:not(:has(> .ws-app-shell__groups))):is(:hover, :focus-within)::before { opacity: 1; transition: inset var(--ws-spring-nav-duration) var(--ws-spring-nav); }
151
197
  :is(.ws-app-shell__groups, .ws-app-shell__nav:not(:has(> .ws-app-shell__groups))):is(:hover, :focus-within) .ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"]) { background: none; box-shadow: none; }
198
+ .ws-app-shell__groups:is(:hover, :focus-within) :is(.ws-app-shell__section:not([open]):has(.ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"])) > summary, .ws-app-shell__fly:has(+ .ws-app-shell__panel :is([aria-current="page"], .ws-menu-item--current))) { background: none; box-shadow: none; }
152
199
  .ws-app-shell__row:is(.ws-app-shell__row--current, [aria-current="page"]) { transition: color var(--ws-duration-hover) var(--ws-ease-soft); }
153
200
  }
154
201
  }
@@ -17,7 +17,7 @@ never coloured, never an app's face.
17
17
 
18
18
  - Shape: a disc, `radius-full`, `overflow: hidden` so a photo never spills past its edge.
19
19
  - Ground: `--ws-surface-sunk`, with navy initials (`--color-text-heading`) semibold, sized to the disc (12 to
20
- 24 px across the sizes below: 12, 14, 14, 16, 24).
20
+ 24 px across the sizes below: 11, 12, 14, 14, 16, 24).
21
21
  - A photo: an `<img alt="">` inside the disc, after the initials, covering it (`object-fit: cover`). The initials stay
22
22
  under it, so the disc is never empty: while it loads, and if it never does. With `data-state="loading"` (the React
23
23
  component sets it, or a page script until `load`) it waits hidden, then fades in over 200 ms (`--ws-duration-gentle`
@@ -26,13 +26,14 @@ never coloured, never an app's face.
26
26
  - `dim`: for someone invited who has not joined yet: the initials in the muted word (`--color-text-muted`, AA on every
27
27
  ground), a photo at 60% opacity. The disc itself never fades, so its initials never drop under AA.
28
28
  - In fello.css: `.ws-avatar` is the 32 px disc with the ring (it mostly sits on a cream card); `--plain` drops the
29
- ring, `--dim` dims it, `--sm`, `--deck`, `--panel` and `--xl` size it. A stack is `.ws-avatar-stack` with
29
+ ring, `--dim` dims it, `--xs`, `--sm`, `--deck`, `--panel` and `--xl` size it. A stack is `.ws-avatar-stack` with
30
30
  `.ws-avatar-stack__more` on the "+N" disc (tabular figures, so "+9" and "+12" sit alike).
31
31
 
32
32
  ## Variants and sizes
33
33
 
34
34
  | Size token | Px | Where |
35
35
  | --- | --- | --- |
36
+ | `xs` | 20 | A face inside a chip, a dense picker's row, a task row's assignee: a line of words' height (#16) |
36
37
  | `sm` | 28 | A dense row (a list's rows, a stack's own faces) |
37
38
  | `md` | 32 | A card's or a list's default face |
38
39
  | `deck` | 48 | A card in a "go through" deck |
@@ -74,7 +75,7 @@ steps `sm` 28, `md` 32, `deck` 48, `panel` 56, `xl` 64. This README and preview
74
75
  | --- | --- | --- |
75
76
  | `name` | `string` | Used for the initials and the alt text context |
76
77
  | `photo` | `string \| null` | A URL to their picture, or `null` for initials only |
77
- | `size` | `'sm' \| 'md' \| 'deck' \| 'panel' \| 'xl'` | The disc's size |
78
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'deck' \| 'panel' \| 'xl'` | The disc's size |
78
79
  | `dim` | `boolean` | Muted initials (a photo at 60%), for someone invited who has not joined |
79
80
  | `ring` | `boolean` | The hairline edge for a cream card |
80
81
 
@@ -100,6 +101,8 @@ ring for a cream card. **Precedent**: the first product built on the system.
100
101
 
101
102
  ```html
102
103
  <span class="ws-avatar ws-avatar--sm" aria-hidden="true">PN</span>
104
+ <!-- extra small: inside a chip or a dense row -->
105
+ <span class="ws-avatar ws-avatar--xs" aria-hidden="true">PN</span>
103
106
 
104
107
  <span class="ws-avatar ws-avatar--plain" aria-hidden="true">DS<img alt="" src="/people/dana.jpg"></span>
105
108
 
@@ -11,8 +11,9 @@
11
11
  <div class="ws-stage ws-stage--column">
12
12
 
13
13
  <div class="ws-stack" style="align-items: flex-start;">
14
- <span class="ws-muted">Sizes: sm 28, md 32, deck 48, panel 56, xl 64 (initials until a photo loads)</span>
14
+ <span class="ws-muted">Sizes: xs 20, sm 28, md 32, deck 48, panel 56, xl 64 (initials until a photo loads)</span>
15
15
  <div class="ws-cluster ws-cluster--group" style="align-items: flex-end;">
16
+ <div class="ws-stack ws-stack--tight" style="align-items: center;"><span class="ws-avatar ws-avatar--plain ws-avatar--xs" aria-hidden="true">PN</span><span class="ws-muted">xs</span></div>
16
17
  <div class="ws-stack ws-stack--tight" style="align-items: center;"><span class="ws-avatar ws-avatar--plain ws-avatar--sm" aria-hidden="true">PN</span><span class="ws-muted">sm</span></div>
17
18
  <div class="ws-stack ws-stack--tight" style="align-items: center;"><span class="ws-avatar ws-avatar--plain" aria-hidden="true">PN</span><span class="ws-muted">md</span></div>
18
19
  <div class="ws-stack ws-stack--tight" style="align-items: center;"><span class="ws-avatar ws-avatar--plain ws-avatar--deck" aria-hidden="true">PN</span><span class="ws-muted">deck</span></div>
@@ -10,5 +10,8 @@
10
10
  house fade of the whole disc left them at 3.7:1); a photo in it fades to 60 %. */
11
11
  .ws-avatar--dim { opacity: 1; color: var(--color-text-muted); }
12
12
  .ws-avatar--dim > img { opacity: 0.6; }
13
+ /* Extra small (#16): 20, a line of words' height, for a face inside a chip, a dense picker's row or a task row's
14
+ assignee: initials 11 px semibold. A stack of them overlaps 4, as every stack. */
15
+ .ws-avatar--xs { width: calc(var(--space-md) + var(--space-xxs)); height: calc(var(--space-md) + var(--space-xxs)); font-size: var(--ws-text-micro); }
13
16
  /* "+N" in a stack: the same disc, tabular figures, so +9 and +12 sit alike. */
14
17
  .ws-avatar-stack__more { font-variant-numeric: tabular-nums; }