@godxjp/ui 28.7.0 → 28.9.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.
Files changed (119) hide show
  1. package/dist/components/data-display/index.d.ts +2 -0
  2. package/dist/components/data-display/index.js +2 -0
  3. package/dist/components/data-display/marquee.d.ts +16 -0
  4. package/dist/components/data-display/marquee.js +155 -0
  5. package/dist/components/general/reveal.d.ts +23 -2
  6. package/dist/components/general/reveal.js +37 -7
  7. package/dist/components/general/typography.d.ts +4 -1
  8. package/dist/components/general/typography.js +14 -1
  9. package/dist/components/layout/affix.d.ts +86 -0
  10. package/dist/components/layout/affix.js +187 -0
  11. package/dist/components/layout/index.d.ts +4 -0
  12. package/dist/components/layout/index.js +4 -0
  13. package/dist/components/layout/legal-document-shell.js +4 -3
  14. package/dist/components/layout/masonry.d.ts +74 -0
  15. package/dist/components/layout/masonry.js +214 -0
  16. package/dist/components/layout/page-container.js +5 -20
  17. package/dist/components/navigation/anchor.d.ts +64 -0
  18. package/dist/components/navigation/anchor.js +284 -0
  19. package/dist/components/navigation/index.d.ts +4 -0
  20. package/dist/components/navigation/index.js +4 -0
  21. package/dist/components/navigation/mega-menu.d.ts +21 -0
  22. package/dist/components/navigation/mega-menu.js +526 -0
  23. package/dist/contracts/measurement.json +1 -1
  24. package/dist/i18n/messages/en.json +517 -0
  25. package/dist/i18n/messages/ja.json +513 -0
  26. package/dist/i18n/messages/vi.json +513 -0
  27. package/dist/lib/hooks.d.ts +68 -0
  28. package/dist/lib/hooks.js +52 -0
  29. package/dist/lib/platform.d.ts +14 -0
  30. package/dist/lib/platform.js +10 -1
  31. package/dist/lib/utils.d.ts +1 -1
  32. package/dist/lib/utils.js +3 -2
  33. package/dist/lib/variants.js +4 -1
  34. package/dist/props/components/data-display.prop.d.ts +95 -1
  35. package/dist/props/components/general.prop.d.ts +47 -3
  36. package/dist/props/components/layout.prop.d.ts +194 -0
  37. package/dist/props/components/navigation.prop.d.ts +263 -0
  38. package/dist/props/registry.d.ts +359 -4
  39. package/dist/props/registry.js +472 -3
  40. package/dist/props/vocabulary/index.d.ts +1 -1
  41. package/dist/props/vocabulary/interaction.prop.d.ts +39 -2
  42. package/dist/props/vocabulary/layout.prop.d.ts +1 -1
  43. package/dist/styles/base.css +47 -14
  44. package/dist/styles/card-layout.css +2 -2
  45. package/dist/styles/chart-layout.css +6 -6
  46. package/dist/styles/control.css +15 -10
  47. package/dist/styles/data-display-layout.css +22 -6
  48. package/dist/styles/density.css +6 -0
  49. package/dist/styles/dialog-layout.css +4 -1
  50. package/dist/styles/focus-ring.css +4 -1
  51. package/dist/styles/layout.css +92 -3
  52. package/dist/styles/motion.css +121 -1
  53. package/dist/styles/navigation-layout.css +397 -1
  54. package/dist/styles/shell-layout.css +28 -21
  55. package/dist/styles/text-layout.css +134 -15
  56. package/dist/tokens/base.css +5 -0
  57. package/dist/tokens/components/activity.css +13 -4
  58. package/dist/tokens/components/affix.css +7 -0
  59. package/dist/tokens/components/anchor.css +17 -0
  60. package/dist/tokens/components/attachments.css +1 -1
  61. package/dist/tokens/components/badge.css +1 -1
  62. package/dist/tokens/components/card.css +28 -7
  63. package/dist/tokens/components/chart.css +4 -1
  64. package/dist/tokens/components/chat-composer.css +4 -1
  65. package/dist/tokens/components/control.css +72 -33
  66. package/dist/tokens/components/conversations.css +4 -1
  67. package/dist/tokens/components/data-display.css +42 -15
  68. package/dist/tokens/components/data-entry.css +8 -2
  69. package/dist/tokens/components/descriptions.css +1 -1
  70. package/dist/tokens/components/feedback.css +8 -5
  71. package/dist/tokens/components/float-button.css +8 -2
  72. package/dist/tokens/components/form.css +1 -1
  73. package/dist/tokens/components/legal-document.css +12 -3
  74. package/dist/tokens/components/logo.css +15 -6
  75. package/dist/tokens/components/marquee.css +7 -0
  76. package/dist/tokens/components/masonry.css +6 -0
  77. package/dist/tokens/components/mega-menu.css +71 -0
  78. package/dist/tokens/components/navigation.css +37 -13
  79. package/dist/tokens/components/segmented.css +4 -1
  80. package/dist/tokens/components/separator.css +4 -1
  81. package/dist/tokens/components/shell.css +99 -31
  82. package/dist/tokens/components/table.css +11 -5
  83. package/dist/tokens/components/thought-chain.css +4 -1
  84. package/dist/tokens/components/toggle.css +4 -1
  85. package/dist/tokens/components/tree.css +1 -1
  86. package/dist/tokens/components/upload.css +21 -9
  87. package/dist/tokens/foundation.css +35 -30
  88. package/dist/tokens/semantic/layout.css +26 -5
  89. package/docs/COMPOSITION-VS-COMPONENT.md +19 -1
  90. package/docs/DESIGN-AUTHORITY.md +99 -18
  91. package/docs/FRAME-COVERAGE-REPORT.md +7 -2
  92. package/docs/TOKENS.md +16 -1
  93. package/docs/data-display/marquee.tsx +254 -0
  94. package/docs/foundation/_theme-editor-scope.ts +222 -0
  95. package/docs/foundation/density.tsx +12 -2
  96. package/docs/foundation/spacing.tsx +5 -0
  97. package/docs/foundation/theme-editor.tsx +645 -0
  98. package/docs/general/activity.tsx +65 -0
  99. package/docs/general/reveal.tsx +290 -22
  100. package/docs/general/typography.tsx +91 -1
  101. package/docs/layout/affix.tsx +209 -0
  102. package/docs/layout/masonry.tsx +291 -0
  103. package/docs/navigation/anchor.tsx +285 -0
  104. package/docs/navigation/mega-menu-panel.tsx +86 -0
  105. package/docs/navigation/mega-menu.tsx +254 -0
  106. package/docs/roadmap/website-components.md +779 -0
  107. package/docs/showcase/acme-website.tsx +75 -39
  108. package/docs/showcase/case4-login.tsx +10 -2
  109. package/docs/showcase/case5-shift-calendar.tsx +1 -1
  110. package/docs/showcase/case6-agency-handy.tsx +6 -6
  111. package/docs/showcase/futurelastic-web.tsx +89 -49
  112. package/docs/showcase/marketing-page.tsx +885 -0
  113. package/docs/showcase/table-expandable-rows.tsx +4 -1
  114. package/docs/showcase/table-footer-totals.tsx +12 -2
  115. package/docs/showcase/theme-customization.tsx +1259 -0
  116. package/package.json +6 -3
  117. package/scripts/brand-accent.generated.mjs +27 -0
  118. package/scripts/ui-audit.mjs +66 -0
  119. package/scripts/visual-audit-rules.mjs +46 -2
@@ -0,0 +1,885 @@
1
+ /**
2
+ * Showcase · Marketing page — the composition doctrine's own deliverable (gh#833).
3
+ *
4
+ * `docs/COMPOSITION-VS-COMPONENT.md` §3 judges Hero, Navbar, Footer, PricingTable, testimonial,
5
+ * logo wall, feature grid and CTA band to be COMPOSITIONS: layout + content + token configuration
6
+ * over real components, expressible today. The doctrine's remedy for a composition is a `docs/`
7
+ * showcase, not a component. Until this page existed that was an assertion. This is the
8
+ * demonstration, and its claim is a MEASUREMENT, not a promise:
9
+ *
10
+ * this page acme-website futurelastic-web
11
+ * `<style>` blocks ................ 0 1 1
12
+ * page-local class SELECTORS ...... 0 17 28
13
+ * bespoke classes ON elements ..... 0 16 25
14
+ * non-`ui-*` classes on elements .. 0 38 45
15
+ * raw px/rem/em/vw literals ....... 0 29 40
16
+ *
17
+ * Counted with comments stripped (so a docblock quoting a class or a length is not counted):
18
+ * selectors are `/(^|[\s,>+~])\.([a-zA-Z_][\w-]*)\s*(?=[{,.:[]|\s)/m`, classes are every token
19
+ * inside a `className`, literals are `/(?<![\w.$}#-])(\d*\.?\d+)(px|rem|em|ch|pt|v\w+)\b/`. The two
20
+ * siblings also RE-THEME, which is half of what their stylesheets carry; this page uses the shipped
21
+ * theme, so everything it could not express is a framework gap rather than a brand decision.
22
+ * `public-landing.tsx` also measures 0/0 and is not a counter-example: it is a narrow
23
+ * `CenteredShell preset="public-landing"` page with no full-bleed band, no display type, no
24
+ * pricing and no logo wall, i.e. none of the shapes that produced the classes above.
25
+ *
26
+ * Every LENGTH here is a token reference: `var(--space-section-band)`, `var(--space-section-hero)`,
27
+ * `var(--page-measure-wide)`, `var(--band-height-3xl)`, `var(--space-6)`, `var(--space-2)`,
28
+ * `var(--overlay-z-index)`. The only class written anywhere below is `.ui-brand-glow`, which the
29
+ * package ships (`src/styles/layout.css`).
30
+ *
31
+ * ── WHAT IS STILL HAND-WRITTEN, AND WHY (the finding this page exists to produce) ───────────────
32
+ *
33
+ * Zero classes and zero literals did NOT mean zero CSS. Six things below are inline `style`
34
+ * objects because no PROP reaches them. Each one is a named gap, in the order a reader meets them:
35
+ *
36
+ * 1. THE SECTION BAND — `paddingBlock: var(--space-section-band | --space-section-hero)`.
37
+ * PARTLY CLOSED by gh#839: the numeric `GapProp` ladder now reaches the two band steps, so
38
+ * `pad={{ block: 20 }}` (80px) and `pad={{ block: 24 }}` (96px) state the rhythm through the
39
+ * public API. The bands below still use an inline `style` because each one ALSO needs the
40
+ * centred inner column (`SHELL`), and nothing owns "full-bleed outside, measured column
41
+ * inside" — that half of gh#839 is open. Neither step has a compact variant either:
42
+ * `--space-section-hero` measures 96px at 1440 and at 320, where the package's own landing
43
+ * preset drops its band via `--centered-shell-landing-main-padding-block-compact`.
44
+ * 2. THE CENTRED COLUMN — `marginInline: auto` + `inlineSize: 100%` +
45
+ * `maxInlineSize: var(--page-measure-wide)` + `paddingInline: var(--space-6)`, repeated at
46
+ * every band. gh#831 minted `--page-measure-wide` and deliberately did not add
47
+ * `PageContainer measure="wide"`, reasoning that a marketing page is full-bleed sections with
48
+ * a centred inner column and `PageContainer` owns neither. The reasoning holds; its
49
+ * consequence is that NOTHING owns the centred inner column.
50
+ * 3. THE STICKY BAR — `position: sticky` + `insetBlockStart` + `zIndex`. `Affix` does not exist
51
+ * here (`docs/roadmap/website-components.md` §5.2 proposed `--affix-inset-block-start` and
52
+ * `--affix-z-index`; neither shipped) and `Topbar` has no `sticky` prop.
53
+ * 4. THE GLASS — `--topbar-background-alpha` / `--topbar-backdrop-blur-size` as inline custom
54
+ * properties. They are `initial` by design, so a call site MUST set them, and that is correct.
55
+ * This used to read `--space-2`, borrowing a token that means something else, because the
56
+ * library had no blur SCALE at all. gh#837 closed that: `--blur-sm|md|lg` now exists in
57
+ * `foundation.css`, deliberately NOT `--scaling`-multiplied, because a backdrop blur is an
58
+ * optical effect and compact density does not mean thinner glass. Measured: `blur(8px)`.
59
+ * 5. THE HALO — `position: relative` / `overflow: hidden` on the band and `position: absolute` /
60
+ * `inset: 0` on the `.ui-brand-glow` layer. This is the package's own idiom (both siblings
61
+ * write `className="ui-brand-glow absolute inset-0"`), spelled as logical inline style so
62
+ * `check:rtl` never has to grant an exception.
63
+ * 6. THE DISPLAY RAMP IS NOT RESPONSIVE, and this is the one that changed the page. gh#826 gave
64
+ * `Heading size` / `Text size` the display steps, and `size` takes a single scalar: there is
65
+ * no responsive object form, the way `Flex direction` and `ResponsiveGrid columns` both take
66
+ * one. `--font-size-5xl` is `--font-size-display` = 54px at 1440 and 54px at 320. Measured
67
+ * with a headline of ordinary marketing length (99 characters): 6 lines at 1280, 10 at 375,
68
+ * 12 at 320, i.e. a phone hero five screens tall. The headline in `en`/`ja`/`vi` was cut to
69
+ * what 54px can carry (2 lines at 1280, 4 at 375) and the long-copy edge case moved to the
70
+ * feature grid, where `features.auditTitle` runs several lines beside `features.ssoTitle` =
71
+ * "SSO". NO page-local `clamp()` was written: both siblings wrote one, and writing a third is
72
+ * how a framework gap becomes folklore. The package already knows the answer for exactly one
73
+ * preset (`--centered-shell-landing-heading-size-compact`, read only by
74
+ * `CenteredShell preset="public-landing"`); a composition cannot reach it.
75
+ *
76
+ * MegaMenu is deliberately ABSENT, and the absence is measured, not an oversight. A site nav and
77
+ * this sticky glass bar are mutually exclusive today. `docs/navigation/mega-menu.tsx` records that
78
+ * MegaMenu must not go in a `Topbar` slot: `topbar-center` is `display: none` below ~1280px, and
79
+ * `topbar-start` is `overflow: clip` with a nowrap row, which put 58 elements outside the viewport
80
+ * at 375px. Meanwhile `.ui-topbar` is the ONLY selector that reads the gh#831 glass knobs, and
81
+ * turning the blur on makes the bar a backdrop root, i.e. the containing block for its own fixed
82
+ * descendants, which is exactly how MegaMenu positions its panel. So the bar carries plain anchors
83
+ * at desktop and the narrow answer is the `Sheet`, which is what the MegaMenu doc prescribes.
84
+ *
85
+ * ── REACHING THE EDGES, AND THE TWO THINGS IT BROKE ─────────────────────────────────────────────
86
+ *
87
+ * Tidy data is why a docs site's examples never break. On purpose here: a 71-character identifier
88
+ * with no break opportunity in it, in three places; CJK beside Latin in one sentence, one plan row
89
+ * and one quote; a feature card with NO description beside five that have one; a testimonial with
90
+ * NO rating beside two that have one; a plan with NO badge and NO figure beside one carrying three
91
+ * badges; a footer column with one link beside one with six; a brand lockup long enough to outgrow
92
+ * the bar. Two of those broke the page, and both fixes are documented at the call site:
93
+ *
94
+ * • The id held the document open at 457px on every phone width. `Text whitespace="pre-wrap"`
95
+ * gives `overflow-wrap: break-word`, which does NOT reduce a box's MIN-CONTENT size, and a
96
+ * flex item keeps `min-width: auto` — so in the footer row it wrapped nothing. `Flex fill`
97
+ * (`flex: 1 1 0` + `min-inline-size: 0`) is what makes the break fire.
98
+ * • The lockup measured 222px against the 85px `topbar-start` had left at 375, and was clipped.
99
+ * `Logo` has no `truncate`; the package's own answer, in `public-landing.tsx`, is to drop the
100
+ * wordmark at phone width.
101
+ *
102
+ * Measured in Chromium after both, at 320 / 375 / 390 / 768 / 1024 / 1280 / 1440:
103
+ * `document.documentElement.scrollWidth` equals the viewport at every one, and zero unclipped
104
+ * elements cross the inline edge. The two boxes that do scroll are the `Marquee` viewport and the
105
+ * `Carousel` viewport, which is what both components are.
106
+ *
107
+ * Every string and every `aria-label` comes from `t()` (`src/i18n/messages/{en,ja,vi}.json`,
108
+ * namespace `marketingShowcase`), and the locale picker in the bar switches all three live. Money
109
+ * is `Intl.NumberFormat` with the ACTIVE locale and an ISO 4217 code that lives in data, never a
110
+ * symbol in the copy. The year is `Intl.DateTimeFormat`. Domains are RFC 2606 reserved
111
+ * (`*.example.jp`), the company is fictional, and every wordmark in the logo wall is invented.
112
+ */
113
+ import { useMemo, useState, type CSSProperties } from "react";
114
+ import {
115
+ ArrowRight,
116
+ CalendarClock,
117
+ CircleCheck,
118
+ FileSearch,
119
+ Globe2,
120
+ KeyRound,
121
+ Menu,
122
+ Route,
123
+ Webhook,
124
+ } from "lucide-react";
125
+
126
+ import {
127
+ Avatar,
128
+ AvatarFallback,
129
+ Badge,
130
+ Card,
131
+ CardContent,
132
+ Carousel,
133
+ CarouselContent,
134
+ CarouselDots,
135
+ CarouselItem,
136
+ CarouselNext,
137
+ CarouselPrevious,
138
+ FeatureList,
139
+ Marquee,
140
+ StatCard,
141
+ } from "@godxjp/ui/data-display";
142
+ import { Rating, Segmented } from "@godxjp/ui/data-entry";
143
+ import {
144
+ Sheet,
145
+ SheetBody,
146
+ SheetClose,
147
+ SheetContent,
148
+ SheetDescription,
149
+ SheetHeader,
150
+ SheetTitle,
151
+ SheetTrigger,
152
+ } from "@godxjp/ui/feedback";
153
+ import { Button, Heading, Icon, Logo, Reveal, Text } from "@godxjp/ui/general";
154
+ import { useTranslation } from "@godxjp/ui/i18n";
155
+ import { Flex, ResponsiveGrid, Separator, Topbar } from "@godxjp/ui/layout";
156
+ import { AppSettingPicker } from "@godxjp/ui/navigation";
157
+
158
+ /* ── The five inline-style constants named in the docblock. Every value is a token reference; no
159
+ number with a unit appears anywhere in this file. ─────────────────────────────────────────── */
160
+
161
+ /** (2) The centred inner column. Nothing in the package owns this shape yet. */
162
+ const SHELL: CSSProperties = {
163
+ marginInline: "auto",
164
+ inlineSize: "100%",
165
+ maxInlineSize: "var(--page-measure-wide)",
166
+ paddingInline: "var(--space-6)",
167
+ };
168
+
169
+ /** (1) The section rhythm gh#831 named and no prop reads. */
170
+ const BAND: CSSProperties = { paddingBlock: "var(--space-section-band)" };
171
+ const HERO_BAND: CSSProperties = { paddingBlock: "var(--space-section-hero)" };
172
+
173
+ /** (5) The halo host and the halo layer. */
174
+ const GLOW_HOST: CSSProperties = { position: "relative", overflow: "hidden" };
175
+ const GLOW_LAYER: CSSProperties = { position: "absolute", inset: 0 };
176
+ const ABOVE_GLOW: CSSProperties = { position: "relative" };
177
+
178
+ /** (3) The sticky bar region. `Affix` does not exist; `Topbar` has no `sticky`. */
179
+ const STICKY_HEADER: CSSProperties = {
180
+ position: "sticky",
181
+ insetBlockStart: 0,
182
+ zIndex: "var(--overlay-z-index)",
183
+ };
184
+
185
+ /** (4) gh#831's glass knobs plus the bar geometry its own docstring recommends. */
186
+ const GLASS_BAR: CSSProperties = {
187
+ "--topbar-height": "var(--band-height-3xl)",
188
+ /* Full-bleed glass, content on the shell measure: the same `max(gutter, half the slack)` the
189
+ package writes for CenteredShell's landing preset, so no page-local media query. */
190
+ "--topbar-inset": "max(var(--space-6), calc((100% - var(--page-measure-wide)) / 2))",
191
+ "--topbar-background-alpha": "85%",
192
+ "--topbar-backdrop-blur-size": "var(--blur-md)",
193
+ } as CSSProperties;
194
+
195
+ /** Per-instance halo shaping — the four `--brand-glow-*` knobs, at the call site. */
196
+ const HERO_GLOW: CSSProperties = {
197
+ "--brand-glow-size": "55% 60%",
198
+ "--brand-glow-position": "82% -8%",
199
+ "--brand-glow-alpha": "0.20",
200
+ } as CSSProperties;
201
+
202
+ const CTA_GLOW: CSSProperties = {
203
+ "--brand-glow-size": "70% 90%",
204
+ "--brand-glow-position": "50% 110%",
205
+ "--brand-glow-alpha": "0.16",
206
+ } as CSSProperties;
207
+
208
+ /* ── Data. RFC 2606 reserved domains, a fictional company, invented customer wordmarks. ──────── */
209
+
210
+ const SITE = "works.example.jp";
211
+
212
+ /**
213
+ * 71 characters, and not one of them is a break opportunity: no hyphen, no underscore, no slash.
214
+ * It is in the hero panel AND in a feature card so the failure has two chances to show itself.
215
+ */
216
+ const ENDPOINT_ID = "whsec0f3a8c1d2e4b5a697c8d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e";
217
+
218
+ const NAV_KEYS = ["product", "solutions", "pricing", "docs"] as const;
219
+
220
+ /** Invented wordmarks. CJK, Latin and Vietnamese diacritics in one row, plus a very long one. */
221
+ const CUSTOMERS = [
222
+ { glyph: "A", name: "Aoba Foods" },
223
+ { glyph: "北", name: "北陸精密工業" },
224
+ { glyph: "M", name: "Meridian Health" },
225
+ { glyph: "T", name: "Trường An" },
226
+ { glyph: "K", name: "Kita Logistics and Warehousing" },
227
+ { glyph: "Ö", name: "Örebro Verkstad" },
228
+ { glyph: "海", name: "海運サービス" },
229
+ ];
230
+
231
+ /** The currency lives in DATA, so nothing in the page hard-codes a symbol or an ISO 4217 code. */
232
+ const CURRENCY = "JPY";
233
+
234
+ type Plan = {
235
+ key: "starter" | "team" | "enterprise";
236
+ /** `null` = quoted: the empty slot beside two filled ones. */
237
+ monthly: number | null;
238
+ yearlyMonthly: number | null;
239
+ accent: boolean;
240
+ badges: Array<"popular" | "saving" | "fresh">;
241
+ features: Array<{ state: "included" | "limited" | "excluded"; key: string; limitKey?: string }>;
242
+ };
243
+
244
+ const PLANS: Plan[] = [
245
+ {
246
+ key: "starter",
247
+ monthly: 4800,
248
+ yearlyMonthly: 4000,
249
+ accent: false,
250
+ badges: [],
251
+ features: [
252
+ { state: "limited", key: "featChains", limitKey: "limitedChains" },
253
+ { state: "included", key: "featExport" },
254
+ { state: "excluded", key: "featDelegation" },
255
+ { state: "excluded", key: "featSso" },
256
+ { state: "excluded", key: "featResidency" },
257
+ { state: "limited", key: "featSupport", limitKey: "limitedSupport" },
258
+ ],
259
+ },
260
+ {
261
+ key: "team",
262
+ monthly: 9600,
263
+ yearlyMonthly: 8000,
264
+ accent: true,
265
+ badges: ["popular", "saving", "fresh"],
266
+ features: [
267
+ { state: "included", key: "featChains" },
268
+ { state: "included", key: "featExport" },
269
+ { state: "included", key: "featDelegation" },
270
+ { state: "included", key: "featSso" },
271
+ { state: "excluded", key: "featResidency" },
272
+ { state: "limited", key: "featSupport", limitKey: "limitedSupport" },
273
+ ],
274
+ },
275
+ {
276
+ key: "enterprise",
277
+ monthly: null,
278
+ yearlyMonthly: null,
279
+ accent: false,
280
+ badges: [],
281
+ features: [
282
+ { state: "included", key: "featChains" },
283
+ { state: "included", key: "featExport" },
284
+ { state: "included", key: "featDelegation" },
285
+ { state: "included", key: "featSso" },
286
+ { state: "included", key: "featResidency" },
287
+ { state: "included", key: "featSupport" },
288
+ ],
289
+ },
290
+ ];
291
+
292
+ const FEATURES = [
293
+ { key: "routing", icon: Route, hasBody: true, long: false },
294
+ /* The long title with NO body: an empty slot beside five filled ones. */
295
+ { key: "audit", icon: FileSearch, hasBody: false, long: true },
296
+ { key: "evidence", icon: CircleCheck, hasBody: true, long: false },
297
+ { key: "webhook", icon: Webhook, hasBody: true, long: false },
298
+ { key: "residency", icon: Globe2, hasBody: true, long: false },
299
+ { key: "sso", icon: KeyRound, hasBody: true, long: false },
300
+ ];
301
+
302
+ const QUOTES = [
303
+ { key: "1", rating: 5 },
304
+ /* Two words, and no rating: the second empty slot. */
305
+ { key: "2", rating: null },
306
+ { key: "3", rating: 4 },
307
+ ] as const;
308
+
309
+ const FOOTER_COLUMNS = [
310
+ { key: "product", links: ["featuresLink", "pricingLink", "changelog", "status"] },
311
+ { key: "company", links: ["about", "careers"] },
312
+ {
313
+ key: "legal",
314
+ links: ["terms", "privacy", "security", "subprocessors", "accessibility", "status"],
315
+ },
316
+ /* One link beside six. */
317
+ { key: "contact", links: ["salesLine"] },
318
+ ];
319
+
320
+ export default function MarketingPage() {
321
+ const { t, locale } = useTranslation();
322
+ const [cycle, setCycle] = useState("monthly");
323
+ const [navOpen, setNavOpen] = useState(false);
324
+
325
+ const money = useMemo(
326
+ () =>
327
+ new Intl.NumberFormat(locale, {
328
+ style: "currency",
329
+ currency: CURRENCY,
330
+ maximumFractionDigits: 0,
331
+ }),
332
+ [locale],
333
+ );
334
+ const count = useMemo(() => new Intl.NumberFormat(locale), [locale]);
335
+ const year = useMemo(
336
+ () => new Intl.DateTimeFormat(locale, { year: "numeric" }).format(new Date()),
337
+ [locale],
338
+ );
339
+
340
+ return (
341
+ <Flex as="div" direction="col" gap="none">
342
+ {/* ── NAVBAR ─────────────────────────────────────────────────────────────────────────── */}
343
+ <header style={STICKY_HEADER}>
344
+ <Topbar
345
+ style={GLASS_BAR}
346
+ start={
347
+ <Flex align="center" gap="lg">
348
+ {/* The lockup measures 222px, and at 375 the start slot has 85px after the end
349
+ cluster takes its natural width — so `topbar-start` clipped it (measured: 93 in
350
+ 85). The bar's shrink contract says a long brand string should truncate rather
351
+ than spill, and `Logo` has no `truncate`; the package's own answer, in
352
+ `public-landing.tsx`, is to drop the wordmark at phone width. The mark alone
353
+ identifies the product there, and the full name is the first thing in the Sheet. */}
354
+ <Flex hideBelow="sm" align="center" gap="none">
355
+ <Logo
356
+ glyph="E"
357
+ wordmark={t("marketingShowcase.brand.name")}
358
+ productSuffix={t("marketingShowcase.brand.suffix")}
359
+ />
360
+ </Flex>
361
+ <Flex hideFrom="sm" align="center" gap="none">
362
+ <Logo glyph="E" label={t("marketingShowcase.brand.name")} />
363
+ </Flex>
364
+ <Flex
365
+ as="div"
366
+ hideBelow="lg"
367
+ align="center"
368
+ gap="xs"
369
+ role="navigation"
370
+ aria-label={t("marketingShowcase.nav.label")}
371
+ >
372
+ {NAV_KEYS.map((key, index) => (
373
+ <Button key={key} variant="ghost" size="sm" asChild>
374
+ <a href={`#${key}`} aria-current={index === 0 ? "page" : undefined}>
375
+ {t(`marketingShowcase.nav.${key}`)}
376
+ </a>
377
+ </Button>
378
+ ))}
379
+ </Flex>
380
+ </Flex>
381
+ }
382
+ end={
383
+ <Flex align="center" gap="sm">
384
+ <AppSettingPicker kind="locale" />
385
+ <Flex hideBelow="sm" align="center" gap="sm">
386
+ <Button variant="ghost" size="sm" asChild>
387
+ <a href="#start">{t("marketingShowcase.nav.signIn")}</a>
388
+ </Button>
389
+ </Flex>
390
+ <Button size="sm" asChild>
391
+ <a href="#start">{t("marketingShowcase.nav.getStarted")}</a>
392
+ </Button>
393
+ {/* The narrow answer the MegaMenu doc prescribes. Nothing in the bar is unreachable
394
+ at 375px: every destination hidden above is inside this Sheet. */}
395
+ <Flex hideFrom="lg" align="center" gap="none">
396
+ <Sheet open={navOpen} onOpenChange={setNavOpen}>
397
+ <SheetTrigger asChild>
398
+ <Button
399
+ variant="ghost"
400
+ size="icon-sm"
401
+ aria-label={t("marketingShowcase.nav.open")}
402
+ >
403
+ <Icon as={Menu} size="sm" />
404
+ </Button>
405
+ </SheetTrigger>
406
+ <SheetContent side="right">
407
+ <SheetHeader>
408
+ <SheetTitle>{t("marketingShowcase.nav.title")}</SheetTitle>
409
+ <SheetDescription>{t("marketingShowcase.nav.description")}</SheetDescription>
410
+ </SheetHeader>
411
+ <SheetBody>
412
+ <Flex
413
+ direction="col"
414
+ gap="xs"
415
+ role="navigation"
416
+ aria-label={t("marketingShowcase.nav.label")}
417
+ >
418
+ {NAV_KEYS.map((key) => (
419
+ <SheetClose key={key} asChild>
420
+ <Button variant="ghost" size="sm" align="start" fullWidth asChild>
421
+ <a href={`#${key}`}>{t(`marketingShowcase.nav.${key}`)}</a>
422
+ </Button>
423
+ </SheetClose>
424
+ ))}
425
+ <Separator space="sm" />
426
+ <SheetClose asChild>
427
+ <Button variant="ghost" size="sm" align="start" fullWidth asChild>
428
+ <a href="#start">{t("marketingShowcase.nav.signIn")}</a>
429
+ </Button>
430
+ </SheetClose>
431
+ </Flex>
432
+ </SheetBody>
433
+ </SheetContent>
434
+ </Sheet>
435
+ </Flex>
436
+ </Flex>
437
+ }
438
+ />
439
+ </header>
440
+
441
+ <main>
442
+ {/* ── HERO ─────────────────────────────────────────────────────────────────────────── */}
443
+ <section id="product" style={GLOW_HOST}>
444
+ <div
445
+ aria-hidden="true"
446
+ className="ui-brand-glow"
447
+ style={{ ...GLOW_LAYER, ...HERO_GLOW }}
448
+ />
449
+ <div style={{ ...SHELL, ...HERO_BAND, ...ABOVE_GLOW }}>
450
+ <ResponsiveGrid columns={{ base: 1, lg: 2 }} gap="xl" align="start">
451
+ <Reveal>
452
+ <Flex direction="col" gap="lg">
453
+ <Flex align="center" gap="sm" wrap>
454
+ <Badge tone="primary" shape="pill">
455
+ {t("marketingShowcase.hero.eyebrow")}
456
+ </Badge>
457
+ </Flex>
458
+ {/* A real <h1> at the display tier — gh#826's `size` beside `level`, instead of
459
+ the `.tx-display` class with a raw font-size both siblings used to carry. */}
460
+ <Heading level={1} size="5xl" weight="bold">
461
+ {t("marketingShowcase.hero.title")}
462
+ </Heading>
463
+ <Text size="xl" tone="muted">
464
+ {t("marketingShowcase.hero.lead")}
465
+ </Text>
466
+ <Flex align="center" gap="sm" wrap>
467
+ <Button size="lg" asChild>
468
+ <a href="#start">
469
+ {t("marketingShowcase.hero.primary")}
470
+ <Icon as={ArrowRight} size="sm" tone="inherit" />
471
+ </a>
472
+ </Button>
473
+ <Button size="lg" variant="outline" asChild>
474
+ <a href="#start">
475
+ <Icon as={CalendarClock} size="sm" tone="inherit" />
476
+ {t("marketingShowcase.hero.secondary")}
477
+ </a>
478
+ </Button>
479
+ </Flex>
480
+ <Text size="xs" tone="muted">
481
+ {t("marketingShowcase.hero.trust")}
482
+ </Text>
483
+ </Flex>
484
+ </Reveal>
485
+
486
+ <Reveal delay={2}>
487
+ <Card variant="outline">
488
+ <CardContent solo>
489
+ <Flex direction="col" gap="md">
490
+ <Flex direction="col" gap="none">
491
+ <Heading level={2} size="md">
492
+ {t("marketingShowcase.hero.panelTitle")}
493
+ </Heading>
494
+ <Text size="xs" tone="muted">
495
+ {t("marketingShowcase.hero.panelNote")}
496
+ </Text>
497
+ </Flex>
498
+ <ResponsiveGrid columns={{ base: 1, sm: 3 }} gap="sm">
499
+ <StatCard
500
+ label={t("marketingShowcase.hero.pending")}
501
+ value={count.format(18)}
502
+ accent="attention"
503
+ />
504
+ <StatCard
505
+ label={t("marketingShowcase.hero.cleared")}
506
+ value={count.format(146)}
507
+ delta="+12"
508
+ />
509
+ <StatCard
510
+ label={t("marketingShowcase.hero.median")}
511
+ value={t("marketingShowcase.hero.medianValue")}
512
+ />
513
+ </ResponsiveGrid>
514
+ <Separator space="xs" />
515
+ <Flex direction="col" gap="xs">
516
+ <Text size="xs" weight="medium" tone="muted">
517
+ {t("marketingShowcase.hero.endpointLabel")}
518
+ </Text>
519
+ {/* 71 characters with nowhere to break. `whitespace="pre-wrap"` is the
520
+ public prop for this (`overflow-wrap: break-word`) and it is enough
521
+ HERE, because a column flex item has a definite inline size to break
522
+ against. In the footer row it was not; see the note there. */}
523
+ <Text as="code" chip size="2xs" whitespace="pre-wrap">
524
+ {ENDPOINT_ID}
525
+ </Text>
526
+ <Text size="2xs" tone="muted">
527
+ {t("marketingShowcase.hero.endpointNote")}
528
+ </Text>
529
+ </Flex>
530
+ </Flex>
531
+ </CardContent>
532
+ </Card>
533
+ </Reveal>
534
+ </ResponsiveGrid>
535
+ </div>
536
+ </section>
537
+
538
+ {/* ── LOGO WALL ────────────────────────────────────────────────────────────────────── */}
539
+ <section aria-labelledby="logos-title">
540
+ <div style={{ ...SHELL, ...BAND }}>
541
+ <Flex direction="col" gap="lg">
542
+ <Flex direction="col" gap="xs">
543
+ <Heading level={2} size="lg" id="logos-title">
544
+ {t("marketingShowcase.logos.title")}
545
+ </Heading>
546
+ <Text size="sm" tone="muted">
547
+ {t("marketingShowcase.logos.caption")}
548
+ </Text>
549
+ </Flex>
550
+ {/* `defaultPlay={false}` is the recommended posture; the pause/play control is
551
+ Marquee's own real Button, in the tab order, with a localized name. */}
552
+ <Marquee
553
+ defaultPlay={false}
554
+ speed="slow"
555
+ gap="xl"
556
+ fade
557
+ pauseOnHover
558
+ label={t("marketingShowcase.logos.marqueeLabel")}
559
+ >
560
+ {CUSTOMERS.map((customer) => (
561
+ <Logo
562
+ key={customer.name}
563
+ glyph={customer.glyph}
564
+ size="sm"
565
+ wordmark={customer.name}
566
+ />
567
+ ))}
568
+ </Marquee>
569
+ </Flex>
570
+ </div>
571
+ </section>
572
+
573
+ {/* ── FEATURE GRID ─────────────────────────────────────────────────────────────────── */}
574
+ <section id="solutions" aria-labelledby="features-title">
575
+ <Flex as="div" direction="col" gap="none" surface="muted">
576
+ <div style={{ ...SHELL, ...BAND }}>
577
+ <Flex direction="col" gap="xl">
578
+ <Flex direction="col" gap="xs">
579
+ <Heading level={2} size="3xl" weight="bold" id="features-title">
580
+ {t("marketingShowcase.features.title")}
581
+ </Heading>
582
+ <Text size="lg" tone="muted">
583
+ {t("marketingShowcase.features.lead")}
584
+ </Text>
585
+ </Flex>
586
+ <ResponsiveGrid columns={{ base: 1, sm: 2, lg: 3 }} gap="md" align="stretch">
587
+ {FEATURES.map((feature) => (
588
+ <Card key={feature.key} hoverable>
589
+ <CardContent solo>
590
+ <Flex direction="col" gap="sm">
591
+ {/* The doctrine's "icon medallion": Avatar square + a Lucide glyph. */}
592
+ <Avatar shape="square" appearance="tinted" size="md">
593
+ <AvatarFallback>
594
+ <Icon as={feature.icon} size="sm" tone="primary" />
595
+ </AvatarFallback>
596
+ </Avatar>
597
+ <Heading level={3} size="md">
598
+ {t(`marketingShowcase.features.${feature.key}Title`)}
599
+ </Heading>
600
+ {feature.hasBody ? (
601
+ <Text size="sm" tone="muted">
602
+ {t(`marketingShowcase.features.${feature.key}Body`)}
603
+ </Text>
604
+ ) : null}
605
+ {feature.key === "webhook" ? (
606
+ <Text as="code" chip size="2xs" whitespace="pre-wrap">
607
+ {ENDPOINT_ID}
608
+ </Text>
609
+ ) : null}
610
+ </Flex>
611
+ </CardContent>
612
+ </Card>
613
+ ))}
614
+ </ResponsiveGrid>
615
+ </Flex>
616
+ </div>
617
+ </Flex>
618
+ </section>
619
+
620
+ {/* ── TESTIMONIALS ─────────────────────────────────────────────────────────────────── */}
621
+ <section aria-labelledby="quotes-title">
622
+ <div style={{ ...SHELL, ...BAND }}>
623
+ <Flex direction="col" gap="lg">
624
+ <Heading level={2} size="3xl" weight="bold" id="quotes-title">
625
+ {t("marketingShowcase.testimonials.title")}
626
+ </Heading>
627
+ <Carousel opts={{ align: "start", loop: true }}>
628
+ <CarouselContent>
629
+ {QUOTES.map((quote) => (
630
+ <CarouselItem key={quote.key}>
631
+ <Card variant="outline">
632
+ <CardContent solo>
633
+ <Flex direction="col" gap="md">
634
+ {quote.rating === null ? null : (
635
+ <Rating
636
+ readOnly
637
+ value={quote.rating}
638
+ name={`quote-${quote.key}`}
639
+ aria-label={t("marketingShowcase.testimonials.ratingLabel")}
640
+ />
641
+ )}
642
+ <Text size="lg">
643
+ {t(`marketingShowcase.testimonials.q${quote.key}`)}
644
+ </Text>
645
+ <Flex align="center" gap="sm">
646
+ <Avatar size="sm">
647
+ <AvatarFallback>
648
+ {t(`marketingShowcase.testimonials.a${quote.key}`).slice(0, 1)}
649
+ </AvatarFallback>
650
+ </Avatar>
651
+ <Flex direction="col" gap="none">
652
+ <Text size="sm" weight="medium">
653
+ {t(`marketingShowcase.testimonials.a${quote.key}`)}
654
+ </Text>
655
+ <Text size="xs" tone="muted">
656
+ {t(`marketingShowcase.testimonials.r${quote.key}`)}
657
+ </Text>
658
+ </Flex>
659
+ </Flex>
660
+ </Flex>
661
+ </CardContent>
662
+ </Card>
663
+ </CarouselItem>
664
+ ))}
665
+ </CarouselContent>
666
+ {/* The three controls are DIRECT children of Carousel because the component
667
+ places them itself: `.ui-carousel-previous` / `-next` are `position: absolute`
668
+ against `.ui-carousel`, and `.ui-carousel-dots` carries its own block padding.
669
+ Wrapping them in a Flex row produced an empty flex item and moved nothing.
670
+ Their accessible names are the component's own (`dataDisplay.carousel.*`). */}
671
+ <CarouselPrevious />
672
+ <CarouselNext />
673
+ <CarouselDots />
674
+ </Carousel>
675
+ </Flex>
676
+ </div>
677
+ </section>
678
+
679
+ {/* ── PRICING ──────────────────────────────────────────────────────────────────────── */}
680
+ <section id="pricing" aria-labelledby="pricing-title">
681
+ <Flex as="div" direction="col" gap="none" surface="muted">
682
+ <div style={{ ...SHELL, ...BAND }}>
683
+ <Flex direction="col" gap="xl">
684
+ <Flex direction="col" gap="md">
685
+ <Heading level={2} size="3xl" weight="bold" id="pricing-title">
686
+ {t("marketingShowcase.pricing.title")}
687
+ </Heading>
688
+ <Text size="lg" tone="muted">
689
+ {t("marketingShowcase.pricing.lead")}
690
+ </Text>
691
+ {/* FeatureList + Card accent + Segmented. The plan said pricing needs nothing
692
+ new, and it needed nothing new. */}
693
+ <Segmented
694
+ value={cycle}
695
+ onValueChange={setCycle}
696
+ aria-label={t("marketingShowcase.pricing.cycleLabel")}
697
+ options={[
698
+ { value: "monthly", label: t("marketingShowcase.pricing.monthly") },
699
+ {
700
+ value: "yearly",
701
+ label: t("marketingShowcase.pricing.yearly"),
702
+ count: 2,
703
+ countLabel: t("marketingShowcase.pricing.saving"),
704
+ },
705
+ ]}
706
+ />
707
+ </Flex>
708
+ <ResponsiveGrid columns={{ base: 1, md: 3 }} gap="md" align="stretch">
709
+ {PLANS.map((plan) => {
710
+ const amount = cycle === "yearly" ? plan.yearlyMonthly : plan.monthly;
711
+ return (
712
+ <Card
713
+ key={plan.key}
714
+ accent={plan.accent ? "primary" : undefined}
715
+ accentPlacement={plan.accent ? "perimeter" : undefined}
716
+ >
717
+ <CardContent solo>
718
+ <Flex direction="col" gap="md">
719
+ <Flex align="center" gap="xs" wrap>
720
+ <Heading level={3} size="lg">
721
+ {t(`marketingShowcase.pricing.${plan.key}Name`)}
722
+ </Heading>
723
+ {plan.badges.map((badge) => (
724
+ <Badge
725
+ key={badge}
726
+ as="span"
727
+ shape="pill"
728
+ tone={badge === "popular" ? "primary" : "muted"}
729
+ variant={badge === "fresh" ? "outline" : "default"}
730
+ >
731
+ {t(`marketingShowcase.pricing.${badge}`)}
732
+ </Badge>
733
+ ))}
734
+ </Flex>
735
+ <Flex direction="col" gap="none">
736
+ {/* The quoted plan has NO figure: an empty slot beside two full
737
+ ones, in the row where a missing number usually collapses. */}
738
+ <Text size="4xl" weight="bold" tabular>
739
+ {amount === null
740
+ ? t("marketingShowcase.pricing.quoted")
741
+ : money.format(amount)}
742
+ </Text>
743
+ <Text size="xs" tone="muted">
744
+ {amount === null
745
+ ? t("marketingShowcase.pricing.quotedNote")
746
+ : cycle === "yearly"
747
+ ? t("marketingShowcase.pricing.billedYearly")
748
+ : t("marketingShowcase.pricing.perMonth")}
749
+ </Text>
750
+ </Flex>
751
+ <Text size="sm" tone="muted">
752
+ {t(`marketingShowcase.pricing.${plan.key}Note`)}
753
+ </Text>
754
+ <FeatureList
755
+ items={plan.features.map((feature) => ({
756
+ state: feature.state,
757
+ label: t(`marketingShowcase.pricing.${feature.key}`),
758
+ description: feature.limitKey
759
+ ? t(`marketingShowcase.pricing.${feature.limitKey}`)
760
+ : undefined,
761
+ }))}
762
+ />
763
+ <Button fullWidth variant={plan.accent ? "default" : "outline"} asChild>
764
+ <a href="#start">
765
+ {amount === null
766
+ ? t("marketingShowcase.pricing.ctaQuote")
767
+ : t("marketingShowcase.pricing.cta")}
768
+ </a>
769
+ </Button>
770
+ </Flex>
771
+ </CardContent>
772
+ </Card>
773
+ );
774
+ })}
775
+ </ResponsiveGrid>
776
+ </Flex>
777
+ </div>
778
+ </Flex>
779
+ </section>
780
+
781
+ {/* ── CTA BAND ─────────────────────────────────────────────────────────────────────── */}
782
+ <section id="start" style={GLOW_HOST} aria-labelledby="cta-title">
783
+ <div
784
+ aria-hidden="true"
785
+ className="ui-brand-glow"
786
+ style={{ ...GLOW_LAYER, ...CTA_GLOW }}
787
+ />
788
+ <div style={{ ...SHELL, ...BAND, ...ABOVE_GLOW }}>
789
+ <Reveal on="view">
790
+ <Flex direction="col" gap="md" align="center">
791
+ <Heading level={2} size="4xl" weight="bold" align="center" id="cta-title">
792
+ {t("marketingShowcase.cta.title")}
793
+ </Heading>
794
+ <Text size="lg" tone="muted" align="center">
795
+ {t("marketingShowcase.cta.lead")}
796
+ </Text>
797
+ <Flex align="center" gap="sm" wrap justify="center">
798
+ <Button size="lg" asChild>
799
+ <a href={`https://${SITE}`}>
800
+ {t("marketingShowcase.cta.primary")}
801
+ <Icon as={ArrowRight} size="sm" tone="inherit" />
802
+ </a>
803
+ </Button>
804
+ <Button size="lg" variant="outline" asChild>
805
+ <a href={`https://${SITE}`}>{t("marketingShowcase.cta.secondary")}</a>
806
+ </Button>
807
+ </Flex>
808
+ <Text size="xs" tone="muted" align="center">
809
+ {t("marketingShowcase.cta.note")}
810
+ </Text>
811
+ </Flex>
812
+ </Reveal>
813
+ </div>
814
+ </section>
815
+ </main>
816
+
817
+ {/* ── FOOTER ───────────────────────────────────────────────────────────────────────── */}
818
+ <footer>
819
+ <Flex as="div" direction="col" gap="none" surface="muted">
820
+ <div style={{ ...SHELL, ...BAND }}>
821
+ <Flex direction="col" gap="lg">
822
+ {/* ResponsiveGrid.Item span — the asymmetric footer both siblings hand-wrote a
823
+ `grid-template-columns: 1.4fr 1fr 1fr 1fr` for. It has shipped the whole time. */}
824
+ <ResponsiveGrid columns={12} gap="lg">
825
+ <ResponsiveGrid.Item span={{ base: 12, sm: 12, md: 4, lg: 4 }}>
826
+ <Flex direction="col" gap="sm">
827
+ <Logo
828
+ glyph="E"
829
+ wordmark={t("marketingShowcase.brand.name")}
830
+ productSuffix={t("marketingShowcase.brand.suffix")}
831
+ />
832
+ <Text size="sm" tone="muted">
833
+ {t("marketingShowcase.footer.addressLabel")} ·{" "}
834
+ {t("marketingShowcase.footer.address")}
835
+ </Text>
836
+ <Text size="sm" tone="muted">
837
+ {t("marketingShowcase.footer.salesLine")} · sales@{SITE}
838
+ </Text>
839
+ </Flex>
840
+ </ResponsiveGrid.Item>
841
+ {FOOTER_COLUMNS.map((column) => (
842
+ <ResponsiveGrid.Item key={column.key} span={{ base: 12, sm: 6, md: 2, lg: 2 }}>
843
+ <Flex
844
+ direction="col"
845
+ gap="xs"
846
+ align="start"
847
+ role="navigation"
848
+ aria-label={t(`marketingShowcase.footer.${column.key}`)}
849
+ >
850
+ <Text size="xs" weight="medium" tone="muted">
851
+ {t(`marketingShowcase.footer.${column.key}`)}
852
+ </Text>
853
+ {column.links.map((link) => (
854
+ <Button key={link} variant="link" size="sm" asChild>
855
+ <a href={`https://${SITE}`}>{t(`marketingShowcase.footer.${link}`)}</a>
856
+ </Button>
857
+ ))}
858
+ </Flex>
859
+ </ResponsiveGrid.Item>
860
+ ))}
861
+ </ResponsiveGrid>
862
+ <Separator space="sm" />
863
+ <Flex align="center" justify="between" gap="sm" wrap>
864
+ <Text size="xs" tone="muted">
865
+ {year} · {t("marketingShowcase.footer.rights")}
866
+ </Text>
867
+ {/* The hostile case, and the one that actually broke: `whitespace="pre-wrap"`
868
+ gives `overflow-wrap: break-word`, which does NOT reduce a box's MIN-CONTENT
869
+ size, and a flex item keeps `min-width: auto`. So in this row the 71-character
870
+ id held the whole line open and the DOCUMENT scrolled: 457px at 390, 375 and
871
+ 320 alike. `Flex fill` is the documented answer (`flex: 1 1 0` +
872
+ `min-inline-size: 0`, layout.css:167) and it is what makes the break fire. */}
873
+ <Flex fill direction="col" gap="none">
874
+ <Text size="2xs" tone="muted" whitespace="pre-wrap" align="end">
875
+ {ENDPOINT_ID}
876
+ </Text>
877
+ </Flex>
878
+ </Flex>
879
+ </Flex>
880
+ </div>
881
+ </Flex>
882
+ </footer>
883
+ </Flex>
884
+ );
885
+ }