@rovela-ai/sdk 0.19.4 → 0.19.5

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 (202) hide show
  1. package/docs/changelog/README.md +53 -0
  2. package/docs/changelog/epoch-01-chrome-consent.md +63 -0
  3. package/docs/changelog/epoch-02-platform-commerce.md +108 -0
  4. package/docs/changelog/epoch-03-i18n-files.md +72 -0
  5. package/docs/changelog/epoch-04-emails.md +65 -0
  6. package/docs/changelog/epoch-05-admin-experience.md +98 -0
  7. package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
  8. package/docs/design/admin-ledger-visual-universe.html +1961 -0
  9. package/docs/design/admin-sidebar-and-band.html +542 -0
  10. package/package.json +3 -1
  11. package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
  12. package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
  13. package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
  14. package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
  15. package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
  16. package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
  17. package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
  18. package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
  19. package/templates/store-template/.env.example +50 -0
  20. package/templates/store-template/.mcp.json +8 -0
  21. package/templates/store-template/.rovela/blueprint.example.json +190 -0
  22. package/templates/store-template/CLAUDE.md +14 -0
  23. package/templates/store-template/app/LayoutContent.tsx +106 -0
  24. package/templates/store-template/app/[...path]/page.tsx +79 -0
  25. package/templates/store-template/app/account/addresses/page.tsx +45 -0
  26. package/templates/store-template/app/account/layout.tsx +17 -0
  27. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
  28. package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
  29. package/templates/store-template/app/account/orders/page.tsx +153 -0
  30. package/templates/store-template/app/account/page.tsx +184 -0
  31. package/templates/store-template/app/account/profile/page.tsx +434 -0
  32. package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
  33. package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
  34. package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
  35. package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
  36. package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
  37. package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
  38. package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
  39. package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
  40. package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
  41. package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
  42. package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
  43. package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
  44. package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
  45. package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
  46. package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
  47. package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
  48. package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
  49. package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
  50. package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
  51. package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
  52. package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
  53. package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
  54. package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
  55. package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
  56. package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
  57. package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
  58. package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
  59. package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
  60. package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
  61. package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
  62. package/templates/store-template/app/admin/layout.tsx +47 -0
  63. package/templates/store-template/app/admin/login/page.tsx +528 -0
  64. package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
  65. package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
  66. package/templates/store-template/app/api/account/addresses/route.ts +10 -0
  67. package/templates/store-template/app/api/account/change-password/route.ts +107 -0
  68. package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
  69. package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
  70. package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
  71. package/templates/store-template/app/api/account/orders/route.ts +29 -0
  72. package/templates/store-template/app/api/account/profile/route.ts +165 -0
  73. package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
  74. package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
  75. package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
  76. package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
  77. package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
  78. package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
  79. package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
  80. package/templates/store-template/app/api/admin/categories/route.ts +9 -0
  81. package/templates/store-template/app/api/admin/check/route.ts +10 -0
  82. package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
  83. package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
  84. package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
  85. package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
  86. package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
  87. package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
  88. package/templates/store-template/app/api/admin/customers/route.ts +9 -0
  89. package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
  90. package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
  91. package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
  92. package/templates/store-template/app/api/admin/emails/route.ts +5 -0
  93. package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
  94. package/templates/store-template/app/api/admin/files/route.ts +8 -0
  95. package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
  96. package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
  97. package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
  98. package/templates/store-template/app/api/admin/locations/route.ts +10 -0
  99. package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
  100. package/templates/store-template/app/api/admin/me/route.ts +11 -0
  101. package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
  102. package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
  103. package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
  104. package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
  105. package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
  106. package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
  107. package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
  108. package/templates/store-template/app/api/admin/orders/route.ts +9 -0
  109. package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
  110. package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
  111. package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
  112. package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
  113. package/templates/store-template/app/api/admin/products/route.ts +9 -0
  114. package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
  115. package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
  116. package/templates/store-template/app/api/admin/settings/route.ts +11 -0
  117. package/templates/store-template/app/api/admin/setup/route.ts +10 -0
  118. package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
  119. package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
  120. package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
  121. package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
  122. package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
  123. package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
  124. package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
  125. package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
  126. package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
  127. package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
  128. package/templates/store-template/app/api/admin/stats/route.ts +9 -0
  129. package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
  130. package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
  131. package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
  132. package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
  133. package/templates/store-template/app/api/admin/users/route.ts +12 -0
  134. package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
  135. package/templates/store-template/app/api/analytics/track/route.ts +12 -0
  136. package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
  137. package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
  138. package/templates/store-template/app/api/auth/register/route.ts +9 -0
  139. package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
  140. package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
  141. package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
  142. package/templates/store-template/app/api/categories/route.ts +10 -0
  143. package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
  144. package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
  145. package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
  146. package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
  147. package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
  148. package/templates/store-template/app/api/checkout/route.ts +10 -0
  149. package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
  150. package/templates/store-template/app/api/checkout/status/route.ts +10 -0
  151. package/templates/store-template/app/api/contact/route.ts +164 -0
  152. package/templates/store-template/app/api/media/presign/route.ts +7 -0
  153. package/templates/store-template/app/api/media/route.ts +7 -0
  154. package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
  155. package/templates/store-template/app/api/products/route.ts +10 -0
  156. package/templates/store-template/app/api/store/settings/route.ts +12 -0
  157. package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
  158. package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
  159. package/templates/store-template/app/auth/layout.tsx +17 -0
  160. package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
  161. package/templates/store-template/app/auth/signin/page.tsx +62 -0
  162. package/templates/store-template/app/auth/signup/page.tsx +38 -0
  163. package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
  164. package/templates/store-template/app/cart/page.tsx +137 -0
  165. package/templates/store-template/app/checkout/page.tsx +88 -0
  166. package/templates/store-template/app/checkout/success/page.tsx +50 -0
  167. package/templates/store-template/app/contact/layout.tsx +24 -0
  168. package/templates/store-template/app/contact/page.tsx +321 -0
  169. package/templates/store-template/app/faq/page.tsx +153 -0
  170. package/templates/store-template/app/globals.css +99 -0
  171. package/templates/store-template/app/layout.tsx +269 -0
  172. package/templates/store-template/app/not-found.tsx +35 -0
  173. package/templates/store-template/app/page.tsx +23 -0
  174. package/templates/store-template/app/privacy/page.tsx +281 -0
  175. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
  176. package/templates/store-template/app/products/[slug]/page.tsx +149 -0
  177. package/templates/store-template/app/products/layout.tsx +26 -0
  178. package/templates/store-template/app/products/page.tsx +102 -0
  179. package/templates/store-template/app/robots.ts +25 -0
  180. package/templates/store-template/app/sitemap.ts +76 -0
  181. package/templates/store-template/app/terms/page.tsx +313 -0
  182. package/templates/store-template/components/PathSyncBridge.tsx +61 -0
  183. package/templates/store-template/components/Providers.tsx +37 -0
  184. package/templates/store-template/components/layout/Footer.tsx +150 -0
  185. package/templates/store-template/components/layout/Header.tsx +162 -0
  186. package/templates/store-template/components/sections/.gitkeep +7 -0
  187. package/templates/store-template/components/sections/CTA.tsx +51 -0
  188. package/templates/store-template/components/sections/Features.tsx +91 -0
  189. package/templates/store-template/components/sections/Hero.tsx +85 -0
  190. package/templates/store-template/eslint.config.mjs +18 -0
  191. package/templates/store-template/lib/store-config.ts +77 -0
  192. package/templates/store-template/lib/store-policy.ts +68 -0
  193. package/templates/store-template/lib/utils.ts +21 -0
  194. package/templates/store-template/next-env.d.ts +6 -0
  195. package/templates/store-template/next.config.js +105 -0
  196. package/templates/store-template/package.json +32 -0
  197. package/templates/store-template/postcss.config.js +6 -0
  198. package/templates/store-template/proxy.ts +150 -0
  199. package/templates/store-template/public/rovela-logo.png +0 -0
  200. package/templates/store-template/tailwind.config.ts +74 -0
  201. package/templates/store-template/tsconfig.json +41 -0
  202. package/templates/store-template/types/next-auth.d.ts +32 -0
@@ -0,0 +1,1961 @@
1
+ <!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1"><style>:root{color-scheme:light}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}</style></head><body>
2
+ <title>Rovela Admin — LEDGER Visual Universe</title>
3
+ <style>
4
+ /* ════════════════════════════════════════════════════════════════
5
+ PAGE CHROME TOKENS (the spec document around the artboards)
6
+ ════════════════════════════════════════════════════════════════ */
7
+ :root{
8
+ --pg-bg:#ECE7DE;
9
+ --pg-card:#F7F3ED;
10
+ --pg-ink:#262F31;
11
+ --pg-muted:#657172;
12
+ --pg-subtle:#8C9899;
13
+ --pg-border:#DCD5C8;
14
+ --pg-accent:#0CAF7D;
15
+ --pg-accent-deep:#087C58;
16
+ --pg-code:#F1ECE3;
17
+ --frame-edge:#D8D1C3;
18
+ }
19
+ @media (prefers-color-scheme: dark){
20
+ :root{
21
+ --pg-bg:#191B1C;
22
+ --pg-card:#212425;
23
+ --pg-ink:#E8E6E1;
24
+ --pg-muted:#9BA3A2;
25
+ --pg-subtle:#6E7877;
26
+ --pg-border:#32373820;
27
+ --pg-border:#343839;
28
+ --pg-code:#1D2021;
29
+ --frame-edge:#3A3F40;
30
+ }
31
+ }
32
+ :root[data-theme="dark"]{
33
+ --pg-bg:#191B1C; --pg-card:#212425; --pg-ink:#E8E6E1; --pg-muted:#9BA3A2;
34
+ --pg-subtle:#6E7877; --pg-border:#343839; --pg-code:#1D2021; --frame-edge:#3A3F40;
35
+ }
36
+ :root[data-theme="light"]{
37
+ --pg-bg:#ECE7DE; --pg-card:#F7F3ED; --pg-ink:#262F31; --pg-muted:#657172;
38
+ --pg-subtle:#8C9899; --pg-border:#DCD5C8; --pg-code:#F1ECE3; --frame-edge:#D8D1C3;
39
+ }
40
+
41
+ *{box-sizing:border-box;margin:0;padding:0}
42
+ html{scroll-behavior:smooth}
43
+ @media (prefers-reduced-motion: reduce){ html{scroll-behavior:auto} *{transition:none!important;animation:none!important} }
44
+
45
+ body{
46
+ background:var(--pg-bg);
47
+ color:var(--pg-ink);
48
+ font-family:'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
49
+ font-size:14px; line-height:1.55;
50
+ -webkit-font-smoothing:antialiased;
51
+ }
52
+ ::selection{background:#0CAF7D33}
53
+
54
+ .wrap{max-width:1240px;margin:0 auto;padding:0 32px}
55
+ @media(max-width:760px){ .wrap{padding:0 16px} }
56
+
57
+ /* ── hero ─────────────────────────────────────────────── */
58
+ .hero{padding:72px 0 40px}
59
+ .hero .eyebrow{font-size:11px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--pg-accent-deep)}
60
+ :root[data-theme="dark"] .hero .eyebrow{color:var(--pg-accent)}
61
+ @media (prefers-color-scheme: dark){ .hero .eyebrow{color:var(--pg-accent)} }
62
+ .hero h1{font-size:clamp(28px,4.5vw,44px);font-weight:700;letter-spacing:-0.02em;line-height:1.12;margin:10px 0 14px;text-wrap:balance}
63
+ .hero p.lede{max-width:62ch;color:var(--pg-muted);font-size:15px}
64
+ .hero .meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:22px}
65
+ .hero .meta span{font-size:12px;font-weight:600;color:var(--pg-muted);border:1px solid var(--pg-border);border-radius:999px;padding:4px 12px;background:var(--pg-card)}
66
+
67
+ /* ── sticky toc ───────────────────────────────────────── */
68
+ .toc{position:sticky;top:0;z-index:50;background:color-mix(in srgb, var(--pg-bg) 88%, transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--pg-border)}
69
+ .toc .wrap{display:flex;gap:4px;overflow-x:auto;padding-top:10px;padding-bottom:10px}
70
+ .toc a{font-size:12.5px;font-weight:600;color:var(--pg-muted);text-decoration:none;padding:6px 12px;border-radius:999px;white-space:nowrap;transition:color .14s,background .14s}
71
+ .toc a:hover{color:var(--pg-ink);background:var(--pg-card)}
72
+ .toc a:focus-visible{outline:2px solid var(--pg-accent);outline-offset:2px}
73
+
74
+ section{padding:56px 0 8px}
75
+ .sec-eyebrow{font-size:11px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--pg-subtle)}
76
+ .sec-title{font-size:24px;font-weight:700;letter-spacing:-0.015em;margin:6px 0 8px}
77
+ .sec-note{color:var(--pg-muted);max-width:70ch;margin-bottom:26px;font-size:14px}
78
+
79
+ /* ── laws grid ────────────────────────────────────────── */
80
+ .laws{display:grid;grid-template-columns:repeat(auto-fill,minmax(268px,1fr));gap:12px}
81
+ .law{background:var(--pg-card);border:1px solid var(--pg-border);border-radius:12px;padding:18px 18px 16px}
82
+ .law .n{font-size:11px;font-weight:700;letter-spacing:.08em;color:var(--pg-accent-deep)}
83
+ @media (prefers-color-scheme: dark){ .law .n{color:var(--pg-accent)} }
84
+ :root[data-theme="dark"] .law .n{color:var(--pg-accent)}
85
+ :root[data-theme="light"] .law .n{color:var(--pg-accent-deep)}
86
+ .law h3{font-size:14px;font-weight:700;margin:6px 0 6px}
87
+ .law p{font-size:12.5px;color:var(--pg-muted);line-height:1.5}
88
+
89
+ /* ── generic spec panels ──────────────────────────────── */
90
+ .panel{background:var(--pg-card);border:1px solid var(--pg-border);border-radius:12px;padding:22px;margin-bottom:14px}
91
+ .panel h3{font-size:13px;font-weight:700;letter-spacing:.02em;margin-bottom:14px;text-transform:uppercase;font-size:11px;letter-spacing:.09em;color:var(--pg-subtle)}
92
+ .cols{display:grid;gap:14px}
93
+ @media(min-width:900px){ .cols-2{grid-template-columns:1fr 1fr} .cols-3{grid-template-columns:1fr 1fr 1fr} }
94
+
95
+ .swatches{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:10px}
96
+ .sw{border:1px solid var(--pg-border);border-radius:10px;overflow:hidden;background:var(--pg-card)}
97
+ .sw .chip{height:52px}
98
+ .sw .lbl{padding:8px 10px}
99
+ .sw .lbl b{display:block;font-size:12px;font-weight:600}
100
+ .sw .lbl span{font-size:11px;color:var(--pg-subtle);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
101
+
102
+ .type-row{display:flex;align-items:baseline;gap:18px;padding:10px 0;border-bottom:1px dashed var(--pg-border)}
103
+ .type-row:last-child{border-bottom:none}
104
+ .type-row .px{width:118px;flex:none;font-size:11px;color:var(--pg-subtle);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
105
+ .type-row .use{margin-left:auto;font-size:11.5px;color:var(--pg-muted);text-align:right;max-width:220px}
106
+
107
+ code, .code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
108
+ pre.code{background:var(--pg-code);border:1px solid var(--pg-border);border-radius:10px;padding:16px;font-size:12px;line-height:1.6;overflow-x:auto;color:var(--pg-ink)}
109
+ pre.code .c{color:var(--pg-subtle)} pre.code .k{color:var(--pg-accent-deep);font-weight:600}
110
+ @media (prefers-color-scheme: dark){ pre.code .k{color:var(--pg-accent)} }
111
+ :root[data-theme="dark"] pre.code .k{color:var(--pg-accent)}
112
+
113
+ /* ════════════════════════════════════════════════════════════════
114
+ ARTBOARD TOKENS (the product itself; deliberately light-only)
115
+ ════════════════════════════════════════════════════════════════ */
116
+ .board{
117
+ --bg:#F7F3ED; --card:#FFFFFF; --sidebar:#F9F7F4; --side-hover:#EFEBE4;
118
+ --ink:#262F31; --muted:#657172; --subtle:#8C9899;
119
+ --border:#E5E0D6; --border-strong:#D8D1C3;
120
+ --accent:#0CAF7D; --accent-hover:#0C9A6E; --accent-deep:#087C58; --accent-tint:#E3F8F0;
121
+ --ok-bg:#E2F5EC; --ok-fg:#0B7A57; --ok:#129B6C;
122
+ --warn-bg:#FCF0D8; --warn-fg:#8A6116; --warn:#E8A213;
123
+ --err-bg:#FCE5E3; --err-fg:#B3372C;
124
+ --info-bg:#E3EFFA; --info-fg:#2563A8; --info:#3372BD;
125
+ --attn-bg:#EEF2D9; --attn-fg:#5F6E1D;
126
+ --edge:0 1px 2px rgba(38,47,49,.04);
127
+ --pop:0 4px 14px -4px rgba(38,47,49,.14), 0 0 0 1px rgba(38,47,49,.03);
128
+ --lift:0 16px 44px -14px rgba(38,47,49,.28);
129
+ --r-ctl:6px; --r-card:10px; --r-modal:12px;
130
+ --t:140ms cubic-bezier(.2,0,0,1);
131
+ background:var(--bg); color:var(--ink);
132
+ font-size:13px; line-height:1.5;
133
+ font-variant-ligatures:none;
134
+ }
135
+ .frame{border:1px solid var(--frame-edge);border-radius:14px;overflow:hidden;box-shadow:0 2px 14px rgba(0,0,0,.05)}
136
+ .frame-bar{display:flex;align-items:center;gap:8px;background:var(--pg-card);border-bottom:1px solid var(--pg-border);padding:9px 14px}
137
+ .frame-bar .dots{display:flex;gap:5px}
138
+ .frame-bar .dots i{width:9px;height:9px;border-radius:50%;background:var(--pg-border);display:block}
139
+ .frame-bar .url{margin-left:6px;font-size:11.5px;color:var(--pg-subtle);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
140
+ .frame-scroll{overflow-x:auto}
141
+ .board{min-width:1080px}
142
+
143
+ .board *{box-sizing:border-box}
144
+ .board .num{font-variant-numeric:tabular-nums}
145
+ .board .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
146
+
147
+ /* ── board: buttons ───────────────────────────────────── */
148
+ .b-btn{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 13px;border-radius:var(--r-ctl);font-size:13px;font-weight:600;font-family:inherit;border:1px solid transparent;cursor:pointer;transition:background var(--t),border-color var(--t),color var(--t),box-shadow var(--t);white-space:nowrap}
149
+ .b-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
150
+ .b-btn svg{width:15px;height:15px;flex:none}
151
+ .b-btn-primary{background:var(--accent);color:#fff}
152
+ .b-btn-primary:hover{background:var(--accent-hover)}
153
+ .b-btn-secondary{background:var(--card);border-color:var(--border-strong);color:var(--ink);box-shadow:var(--edge)}
154
+ .b-btn-secondary:hover{border-color:#C9C1B1;background:#FDFCFA}
155
+ .b-btn-ghost{background:transparent;color:var(--muted)}
156
+ .b-btn-ghost:hover{background:var(--side-hover);color:var(--ink)}
157
+ .b-btn-danger{background:transparent;border-color:transparent;color:var(--err-fg)}
158
+ .b-btn-danger:hover{background:var(--err-bg)}
159
+ .b-btn-danger-solid{background:#C24A3F;color:#fff}
160
+ .b-btn-danger-solid:hover{background:#B3372C}
161
+ .b-btn-sm{height:28px;padding:0 10px;font-size:12.5px}
162
+ .b-btn-icon{width:28px;height:28px;padding:0;justify-content:center;color:var(--subtle)}
163
+ .b-btn-icon:hover{background:var(--side-hover);color:var(--ink)}
164
+
165
+ /* ── board: inputs & selects ──────────────────────────── */
166
+ .b-input{display:flex;align-items:center;gap:8px;height:32px;padding:0 10px;background:var(--card);border:1px solid var(--border-strong);border-radius:var(--r-ctl);font-size:13px;color:var(--ink);width:100%;font-family:inherit;transition:border-color var(--t),box-shadow var(--t)}
167
+ .b-input:focus,.b-input:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px #0CAF7D22;outline:none}
168
+ .b-input input{border:none;outline:none;background:transparent;font:inherit;color:inherit;width:100%}
169
+ .b-input input::placeholder{color:var(--subtle)}
170
+ .b-input svg{width:14px;height:14px;color:var(--subtle);flex:none}
171
+ textarea.b-input{height:auto;min-height:72px;padding:8px 10px;resize:vertical;display:block}
172
+ .b-select{display:inline-flex;align-items:center;justify-content:space-between;gap:8px;height:32px;padding:0 9px 0 11px;background:var(--card);border:1px solid var(--border-strong);border-radius:var(--r-ctl);font-size:13px;font-weight:500;color:var(--ink);cursor:pointer;transition:border-color var(--t);user-select:none}
173
+ .b-select:hover{border-color:#C9C1B1}
174
+ .b-select svg{width:13px;height:13px;color:var(--subtle)}
175
+ .b-label{display:block;font-size:12.5px;font-weight:600;margin-bottom:6px}
176
+ .b-help{font-size:12px;color:var(--subtle);margin-top:5px}
177
+ .b-field{margin-bottom:16px}
178
+ .b-field:last-child{margin-bottom:0}
179
+
180
+ /* ── board: badges, chips ─────────────────────────────── */
181
+ .b-badge{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:999px;font-size:11.5px;font-weight:600;letter-spacing:.01em}
182
+ .b-badge-ok{background:var(--ok-bg);color:var(--ok-fg)}
183
+ .b-badge-warn{background:var(--warn-bg);color:var(--warn-fg)}
184
+ .b-badge-err{background:var(--err-bg);color:var(--err-fg)}
185
+ .b-badge-info{background:var(--info-bg);color:var(--info-fg)}
186
+ .b-badge-attn{background:var(--attn-bg);color:var(--attn-fg)}
187
+ .b-badge-mute{background:#EFEBE4;color:var(--muted)}
188
+ .b-chip{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;font-size:12.5px;font-weight:600;color:var(--muted);background:transparent;border:1px solid transparent;cursor:pointer;transition:background var(--t),color var(--t)}
189
+ .b-chip:hover{background:var(--side-hover);color:var(--ink)}
190
+ .b-chip.on{background:var(--accent-tint);color:var(--accent-deep)}
191
+ .b-delta{display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 7px;border-radius:999px;font-size:11px;font-weight:700}
192
+ .b-delta svg{width:10px;height:10px}
193
+ .b-delta.up{background:var(--ok-bg);color:var(--ok-fg)}
194
+ .b-delta.down{background:var(--err-bg);color:var(--err-fg)}
195
+ .b-delta.flat{background:#EFEBE4;color:var(--subtle)}
196
+
197
+ /* ── board: sidebar ───────────────────────────────────── */
198
+ .b-side{width:224px;flex:none;background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:640px}
199
+ .b-side-brand{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--border)}
200
+ .b-side-brand .logo{width:28px;height:28px;border-radius:7px;background:var(--ink);color:#F7F3ED;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px}
201
+ .b-side-brand b{display:block;font-size:13px;font-weight:700;line-height:1.2}
202
+ .b-side-brand span{font-size:11px;color:var(--subtle)}
203
+ .b-side-nav{padding:10px 8px;flex:1}
204
+ .b-side-eyebrow{font-size:10.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--subtle);padding:14px 10px 5px}
205
+ .b-side a{display:flex;align-items:center;gap:9px;height:30px;padding:0 10px;border-radius:6px;font-size:13px;font-weight:500;color:var(--muted);text-decoration:none;transition:background var(--t),color var(--t)}
206
+ .b-side a:hover{background:var(--side-hover);color:var(--ink)}
207
+ .b-side a.on{background:var(--accent-tint);color:var(--accent-deep);font-weight:600}
208
+ .b-side a svg{width:15px;height:15px;flex:none}
209
+ .b-side-foot{border-top:1px solid var(--border);padding:10px 8px}
210
+ .b-side-user{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:6px}
211
+ .b-side-user:hover{background:var(--side-hover)}
212
+ .b-side-user .av{width:24px;height:24px;border-radius:50%;background:var(--accent-tint);color:var(--accent-deep);font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center}
213
+ .b-side-user b{font-size:12.5px;font-weight:600}
214
+
215
+ .b-main{flex:1;min-width:0;padding:22px 28px 30px;position:relative}
216
+ .b-shell{display:flex;position:relative;overflow:hidden}
217
+
218
+ /* ── board: page header ───────────────────────────────── */
219
+ .b-phead{display:flex;align-items:flex-start;gap:12px;margin-bottom:20px}
220
+ .b-phead .back{width:28px;height:28px;border-radius:6px;border:1px solid var(--border-strong);background:var(--card);display:flex;align-items:center;justify-content:center;color:var(--muted);cursor:pointer;flex:none;margin-top:2px;transition:background var(--t)}
221
+ .b-phead .back:hover{background:var(--side-hover);color:var(--ink)}
222
+ .b-phead .back svg{width:15px;height:15px}
223
+ .b-phead .tt{min-width:0}
224
+ .b-phead .eyebrow{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:2px}
225
+ .b-phead h1{font-size:20px;font-weight:700;letter-spacing:-0.01em;line-height:1.25;display:flex;align-items:center;gap:10px}
226
+ .b-phead .sub{font-size:12.5px;color:var(--muted);margin-top:2px}
227
+ .b-phead .acts{margin-left:auto;display:flex;align-items:center;gap:8px;flex:none}
228
+
229
+ /* ── board: cards ─────────────────────────────────────── */
230
+ .b-card{background:var(--card);border:1px solid var(--border);border-radius:var(--r-card);box-shadow:var(--edge)}
231
+ .b-card-pad{padding:18px}
232
+ .b-card-title{font-size:14px;font-weight:700;margin-bottom:2px}
233
+ .b-card-sub{font-size:12px;color:var(--subtle)}
234
+
235
+ /* ── board: KPI ───────────────────────────────────────── */
236
+ .b-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:12px}
237
+ .b-kpi{background:var(--card);border:1px solid var(--border);border-radius:var(--r-card);box-shadow:var(--edge);padding:14px 16px 13px}
238
+ .b-kpi .k{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--subtle)}
239
+ .b-kpi .v{font-size:20px;font-weight:700;letter-spacing:-0.01em;margin:5px 0 7px}
240
+ .b-kpi .m{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--subtle)}
241
+
242
+ /* ── board: tables ────────────────────────────────────── */
243
+ .b-table{width:100%;border-collapse:collapse;font-size:13px}
244
+ .b-table th{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--subtle);text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);white-space:nowrap}
245
+ .b-table td{padding:0 12px;height:44px;border-bottom:1px solid var(--border);vertical-align:middle;white-space:nowrap}
246
+ .b-table tr:last-child td{border-bottom:none}
247
+ .b-table tbody tr{transition:background var(--t);cursor:pointer}
248
+ .b-table tbody tr:hover{background:#FBF9F5}
249
+ .b-table .r{text-align:right}
250
+ .b-table .muted{color:var(--muted)}
251
+ .b-check{width:15px;height:15px;border:1.5px solid var(--border-strong);border-radius:4px;background:var(--card);appearance:none;cursor:pointer;display:inline-block;vertical-align:middle;position:relative;transition:background var(--t),border-color var(--t)}
252
+ .b-check:checked{background:var(--accent);border-color:var(--accent)}
253
+ .b-check:checked::after{content:'';position:absolute;left:4px;top:1px;width:4px;height:8px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
254
+ .b-check:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
255
+
256
+ /* ── board: filter row / bulk bar ─────────────────────── */
257
+ .b-filters{display:flex;align-items:center;gap:8px;margin-bottom:12px;min-height:32px}
258
+ .b-filters .grow{flex:1}
259
+ .b-bulk{display:none;align-items:center;gap:10px;background:var(--ink);color:#F5F2EC;border-radius:8px;padding:0 8px 0 14px;height:40px;font-size:12.5px;font-weight:600}
260
+ .b-bulk .b-btn{background:transparent;border:1px solid #4A5456;color:#E8E6E1;height:26px;font-size:12px}
261
+ .b-bulk .b-btn:hover{background:#3A4345}
262
+ .b-bulk .sp{flex:1}
263
+ .b-bulk .x{color:#9BA3A2;cursor:pointer;display:flex}
264
+ .b-bulk .x:hover{color:#fff}
265
+ .b-listrow.bulk-on .b-filters{display:none}
266
+ .b-listrow.bulk-on .b-bulk{display:flex;margin-bottom:12px}
267
+
268
+ /* ── board: row menu (⋯) ──────────────────────────────── */
269
+ .b-menu-wrap{position:relative;display:inline-block}
270
+ .b-menu{position:absolute;right:0;top:32px;z-index:30;min-width:168px;background:var(--card);border:1px solid var(--border);border-radius:8px;box-shadow:var(--pop);padding:5px;display:none}
271
+ .b-menu.open{display:block}
272
+ .b-menu button{display:flex;align-items:center;gap:9px;width:100%;padding:7px 9px;border:none;background:transparent;border-radius:5px;font:inherit;font-size:12.5px;font-weight:500;color:var(--ink);cursor:pointer;text-align:left;transition:background var(--t)}
273
+ .b-menu button:hover{background:var(--side-hover)}
274
+ .b-menu button.danger{color:var(--err-fg)}
275
+ .b-menu button.danger:hover{background:var(--err-bg)}
276
+ .b-menu button svg{width:14px;height:14px;color:var(--subtle)}
277
+ .b-menu button.danger svg{color:var(--err-fg)}
278
+ .b-menu hr{border:none;border-top:1px solid var(--border);margin:5px 4px}
279
+
280
+ /* ── board: save bar — the ATTENTION BAND ─────────────── */
281
+ /* The top bar is the identity band (emerald wash, one hairline). A dirty
282
+ document raises its bottom bookend: a DOCKED full-width surface in the
283
+ sidebar tone with an amber wash + top hairline. Never a floating dark
284
+ pill — in this system bands are washed surfaces; ink is for toasts. */
285
+ .b-savebar{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:10px;height:52px;padding:0 16px 0 18px;background:linear-gradient(to top, rgba(232,162,19,.08), rgba(232,162,19,.015)),#F9F7F4;border-top:1px solid rgba(38,47,49,.12);transform:translateY(102%);transition:transform 200ms cubic-bezier(.2,0,0,1);z-index:40}
286
+ .b-savebar.show{transform:none}
287
+ .b-savebar .dot{width:8px;height:8px;border-radius:50%;background:var(--warn);flex:none}
288
+ .b-savebar .lbl{font-size:13px;font-weight:700;white-space:nowrap}
289
+ .b-savebar .meta{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
290
+ .b-savebar .sp{flex:1}
291
+ .b-savebar .b-btn{height:30px}
292
+ .b-savebar.saving .dot{background:var(--subtle)}
293
+ .b-savebar.saved{background:linear-gradient(to top, rgba(18,155,108,.10), rgba(18,155,108,.02)),#F9F7F4}
294
+ .b-savebar.saved .dot{background:var(--ok)}
295
+ .b-savebar.error-state{background:linear-gradient(to top, rgba(179,55,44,.09), rgba(179,55,44,.02)),#F9F7F4}
296
+ .b-savebar.error-state .dot{background:#B3372C}
297
+ /* Nav-guard: clicking away while dirty SHAKES the bar instead of silently
298
+ losing work (the beloved Shopify behavior). */
299
+ .b-savebar.shake{animation:sb-shake 320ms cubic-bezier(.36,.07,.19,.97)}
300
+ @keyframes sb-shake{10%,90%{transform:translateX(-2px)}30%,70%{transform:translateX(5px)}50%{transform:translateX(-5px)}}
301
+
302
+ /* ── board: modal ─────────────────────────────────────── */
303
+ .b-overlay{position:absolute;inset:0;background:rgba(38,47,49,.32);display:none;align-items:center;justify-content:center;z-index:60;backdrop-filter:blur(1px)}
304
+ .b-overlay.open{display:flex}
305
+ .b-modal{width:400px;background:var(--card);border-radius:var(--r-modal);box-shadow:var(--lift);padding:20px;transform:translateY(4px) scale(.985);opacity:0;transition:transform 160ms cubic-bezier(.2,0,0,1),opacity 160ms}
306
+ .b-overlay.open .b-modal{transform:none;opacity:1}
307
+ .b-modal h4{font-size:15px;font-weight:700;margin-bottom:6px}
308
+ .b-modal p{font-size:13px;color:var(--muted);margin-bottom:16px}
309
+ .b-modal p b{color:var(--ink);font-weight:600}
310
+ .b-modal .acts{display:flex;justify-content:flex-end;gap:8px}
311
+
312
+ /* ── board: toast — THE notification (events only) ────── */
313
+ /* The toast is the FLOATING-CARD sibling of the save bar: same washed
314
+ surface language (green wash = success, red wash = error), ink text,
315
+ hairline border. It floats, so it earns the popover shadow. */
316
+ .b-toast{position:absolute;bottom:18px;right:18px;display:flex;align-items:center;gap:9px;background:linear-gradient(to top, rgba(18,155,108,.10), rgba(18,155,108,.02)),#fff;color:var(--ink);border:1px solid var(--border);font-size:12.5px;font-weight:600;border-radius:8px;padding:10px 14px;box-shadow:var(--pop);transform:translateY(64px);opacity:0;transition:transform 200ms cubic-bezier(.2,0,0,1),opacity 200ms;z-index:70}
317
+ .b-toast.show{transform:none;opacity:1}
318
+ .b-toast svg{width:14px;height:14px;color:#0B7A57;flex:none}
319
+ .b-toast.err{background:linear-gradient(to top, rgba(179,55,44,.10), rgba(179,55,44,.02)),#fff}
320
+ .b-toast.err svg{color:#B3372C}
321
+ .b-toast-action{margin-left:4px;color:var(--accent-deep);font-size:12px;font-weight:700;cursor:pointer;text-decoration:underline;text-underline-offset:2px}
322
+ .b-toast-x{margin-left:6px;display:inline-flex;color:var(--subtle);cursor:pointer}
323
+ .b-toast-x:hover{color:var(--ink)}
324
+ /* Stacked toaster (playground): a fixed bottom-right column inside the
325
+ board; newest at the bottom, max 3, success auto-dismisses, error is
326
+ sticky with a dismiss. */
327
+ .b-toaster{position:absolute;bottom:14px;right:14px;display:flex;flex-direction:column;gap:8px;align-items:flex-end;z-index:80}
328
+ .b-toaster .b-toast{position:static;transform:none;opacity:1;animation:toast-in 200ms cubic-bezier(.2,0,0,1)}
329
+ @keyframes toast-in{from{transform:translateY(20px);opacity:0}to{transform:none;opacity:1}}
330
+ .b-toaster .b-toast.leaving{opacity:0;transform:translateY(8px)}
331
+
332
+ /* ── board: misc ──────────────────────────────────────── */
333
+ .b-empty{display:flex;flex-direction:column;align-items:center;text-align:center;padding:44px 20px}
334
+ .b-empty .ic{width:38px;height:38px;border-radius:10px;background:var(--accent-tint);color:var(--accent-deep);display:flex;align-items:center;justify-content:center;margin-bottom:12px}
335
+ .b-empty .ic svg{width:18px;height:18px}
336
+ .b-empty b{font-size:14px;font-weight:700}
337
+ .b-empty p{font-size:12.5px;color:var(--muted);margin:4px 0 14px;max-width:34ch}
338
+ .b-skel{height:12px;border-radius:5px;background:linear-gradient(90deg,#EFEBE4 25%,#F6F2EB 45%,#EFEBE4 65%);background-size:220% 100%;animation:shimmer 1.4s linear infinite}
339
+ @keyframes shimmer{from{background-position:120% 0}to{background-position:-120% 0}}
340
+ .b-tabs{display:flex;gap:2px;border-bottom:1px solid var(--border);margin-bottom:16px}
341
+ .b-tabs button{border:none;background:none;font:inherit;font-size:13px;font-weight:600;color:var(--muted);padding:8px 12px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color var(--t)}
342
+ .b-tabs button:hover{color:var(--ink)}
343
+ .b-tabs button.on{color:var(--ink);border-bottom-color:var(--accent)}
344
+ .b-seg{display:inline-flex;background:var(--card);border:1px solid var(--border-strong);border-radius:7px;padding:2px;gap:1px}
345
+ .b-seg button{border:none;background:none;font:inherit;font-size:12px;font-weight:600;color:var(--muted);height:26px;padding:0 11px;border-radius:5px;cursor:pointer;transition:background var(--t),color var(--t)}
346
+ .b-seg button:hover{color:var(--ink)}
347
+ .b-seg button.on{background:var(--accent-tint);color:var(--accent-deep)}
348
+ .b-kbd{display:inline-flex;align-items:center;gap:2px;font-size:10.5px;font-weight:600;color:var(--subtle);border:1px solid var(--border-strong);border-bottom-width:2px;border-radius:4px;padding:1px 5px;background:var(--card)}
349
+
350
+ /* ── board: AUTH — the front door ─────────────────────── */
351
+ /* One shared shell for all five doors: cream page with the identity wash
352
+ greeting you at the top, brand tile (favicon-first) + store name +
353
+ ADMIN chip, a single 380px card, powered-by underneath. */
354
+ .v4{display:block;min-height:640px;background:linear-gradient(to bottom, rgba(12,175,125,.055), rgba(12,175,125,0) 220px),#F7F3ED;padding:0}
355
+ .v4-grid{display:grid;grid-template-columns:1fr 400px;gap:0;min-height:640px}
356
+ .v4-main{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 24px}
357
+ .v4-rail{border-left:1px solid var(--border);background:#F3EEE6;padding:22px;overflow-y:auto;max-height:640px}
358
+ .v4-rail-label{font-size:10.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--subtle);margin-bottom:12px}
359
+ .v4-brand{display:flex;flex-direction:column;align-items:center;gap:10px;margin-bottom:22px}
360
+ .v4-tile{width:40px;height:40px;border-radius:9px;background:#fff;border:1px solid var(--border);box-shadow:0 1px 2px rgba(38,47,49,.05);display:flex;align-items:center;justify-content:center}
361
+ .v4-tile span{font-weight:800;font-size:17px;color:#C0392B}
362
+ .v4-brand .nm{display:flex;align-items:center;gap:8px}
363
+ .v4-brand .nm b{font-size:15px;font-weight:700}
364
+ .v4-brand .nm i{font-style:normal;font-size:9.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);background:#EFEBE4;border-radius:999px;padding:2px 7px}
365
+ .v4-card{width:380px;max-width:100%;background:#fff;border:1px solid var(--border);border-radius:10px;box-shadow:var(--edge);padding:24px}
366
+ .v4-card h1{font-size:20px;font-weight:700;letter-spacing:-.01em}
367
+ .v4-card .sub{font-size:12.5px;color:var(--muted);margin:4px 0 18px}
368
+ .v4-field{margin-bottom:14px}
369
+ .v4-lblrow{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
370
+ .v4-lbl{font-size:12.5px;font-weight:600}
371
+ .v4-forgot{font-size:12px;font-weight:600;color:var(--accent-deep);cursor:pointer;text-decoration:none}
372
+ .v4-forgot:hover{text-decoration:underline}
373
+ .v4-input{position:relative;display:flex;align-items:center}
374
+ .v4-input input{width:100%;height:36px;padding:0 11px;background:#fff;border:1px solid #D8D1C3;border-radius:6px;font:inherit;font-size:13px;color:var(--ink);transition:border-color .14s,box-shadow .14s}
375
+ .v4-input input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(12,175,125,.13)}
376
+ .v4-eye{position:absolute;right:9px;color:var(--subtle);cursor:pointer;display:flex}
377
+ .v4-eye:hover{color:var(--ink)}
378
+ .v4-remember{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted);margin:2px 0 16px;cursor:pointer;user-select:none}
379
+ .v4-submit{width:100%;height:36px;border:none;border-radius:6px;background:var(--accent);color:#fff;font:inherit;font-size:13px;font-weight:700;cursor:pointer;transition:background .14s}
380
+ .v4-submit:hover{background:var(--accent-hover)}
381
+ .v4-submit:disabled{opacity:.75;cursor:default}
382
+ .v4-alert{display:none;background:linear-gradient(to top, rgba(179,55,44,.08), rgba(179,55,44,.02)),#fff;border:1px solid rgba(179,55,44,.25);border-radius:6px;padding:9px 11px;font-size:12.5px;color:#B3372C;font-weight:600;margin-bottom:14px}
383
+ .v4-alert.show{display:block}
384
+ .v4-pow{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--subtle);margin-top:20px}
385
+ .v4-pow b{color:var(--accent-deep)}
386
+ .v4-mini{background:#fff;border:1px solid var(--border);border-radius:9px;box-shadow:var(--edge);padding:16px;margin-bottom:12px}
387
+ .v4-mini .cap{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:8px}
388
+ .v4-mini h2{font-size:15px;font-weight:700;letter-spacing:-.01em}
389
+ .v4-mini .s{font-size:11px;color:var(--muted);margin:3px 0 10px}
390
+ .v4-mini .f{height:26px;border:1px solid #E0DACE;border-radius:5px;margin-bottom:8px;background:#FDFCFA}
391
+ .v4-mini .b{height:26px;border-radius:5px;background:var(--accent);opacity:.92;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700}
392
+ .v4-mini .okwash{display:flex;align-items:center;gap:8px;background:linear-gradient(to top, rgba(18,155,108,.10), rgba(18,155,108,.02)),#fff;border:1px solid var(--border);border-radius:6px;padding:9px 11px;font-size:11.5px;font-weight:600;color:#0B7A57}
393
+
394
+ /* ── board: ONE-WAY-EACH consolidation kit (tab 07) ───────── */
395
+ .os-cell{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:18px;box-shadow:var(--edge)}
396
+ .os-lab{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:12px;display:flex;align-items:center;gap:8px}
397
+ .os-lab .fix{margin-left:auto;font-size:10px;letter-spacing:.02em;text-transform:none;font-weight:700;color:var(--accent-deep);background:var(--accent-tint);border-radius:999px;padding:2px 8px;white-space:nowrap}
398
+ .os-note{font-size:12px;color:var(--muted);line-height:1.55;margin-top:12px}
399
+ .os-note b{color:var(--ink)}
400
+ .os-beh{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
401
+ .os-beh span{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:700;color:var(--muted);border:1px solid var(--border-strong);border-radius:999px;padding:3px 9px;background:var(--card)}
402
+ .os-beh span svg{width:10px;height:10px;color:var(--ok)}
403
+ .b-spin{display:inline-block;border:2px solid rgba(38,47,49,.14);border-top-color:var(--accent);border-radius:50%;width:16px;height:16px;animation:b-rot .7s linear infinite;flex:none}
404
+ .b-spin.sm{width:12px;height:12px;border-width:1.5px}
405
+ .b-spin.lg{width:22px;height:22px}
406
+ .b-spin.inv{border-color:rgba(255,255,255,.35);border-top-color:#fff}
407
+ @keyframes b-rot{to{transform:rotate(360deg)}}
408
+ .b-switch{position:relative;width:34px;height:20px;border-radius:999px;background:#D8D1C3;border:none;cursor:pointer;transition:background var(--t);flex:none;padding:0}
409
+ .b-switch::after{content:'';position:absolute;left:2px;top:2px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(38,47,49,.22);transition:transform var(--t)}
410
+ .b-switch[aria-checked="true"]{background:var(--accent)}
411
+ .b-switch[aria-checked="true"]::after{transform:translateX(14px)}
412
+ .b-switch:disabled{opacity:.45;cursor:default}
413
+ .b-switch:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
414
+ .os-addon{display:flex;align-items:stretch;width:100%}
415
+ .os-addon .pre{display:flex;align-items:center;padding:0 10px;background:#F3EEE6;border:1px solid var(--border-strong);border-right:none;border-radius:6px 0 0 6px;font-size:12.5px;color:var(--muted);font-weight:600}
416
+ .os-addon input{flex:1;min-width:0;height:32px;padding:0 10px;border:1px solid var(--border-strong);border-radius:0 6px 6px 0;font:inherit;font-size:13px;color:var(--ink);text-align:right;font-variant-numeric:tabular-nums}
417
+ .os-addon input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px #0CAF7D22}
418
+ .os-vs{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--border);border-radius:8px;overflow:hidden;font-size:12.5px}
419
+ .os-vs > div{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center}
420
+ .os-vs > div:nth-child(odd){border-right:1px solid var(--border);color:var(--subtle);background:#FBF9F5;text-decoration:line-through}
421
+ .os-vs > div:nth-last-child(-n+2){border-bottom:none}
422
+ .os-pagerow{display:flex;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--border);margin-top:-1px}
423
+ .os-pagerow .info{font-size:12px;color:var(--muted)}
424
+ .os-alert{display:flex;align-items:center;gap:8px;background:linear-gradient(to top, rgba(179,55,44,.08), rgba(179,55,44,.02)),#fff;border:1px solid rgba(179,55,44,.25);border-radius:6px;padding:8px 11px;font-size:12.5px;color:#B3372C;font-weight:600;margin-bottom:14px}
425
+
426
+ /* ── board: MOBILE kit (tab 08) — phone frames ─────────── */
427
+ .ph{width:350px;flex:none;background:#F7F3ED;border:1px solid var(--frame-edge);border-radius:26px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,.07);position:relative}
428
+ .ph-status{display:flex;align-items:center;justify-content:space-between;padding:8px 18px 4px;font-size:10.5px;font-weight:700;color:var(--ink)}
429
+ .ph-status i{font-style:normal;letter-spacing:.06em;color:var(--subtle);font-weight:600}
430
+ .ph-body{position:relative;height:640px;overflow:hidden;display:flex;flex-direction:column;background:#F7F3ED}
431
+ .ph-home{position:absolute;left:50%;bottom:6px;transform:translateX(-50%);width:110px;height:4px;border-radius:999px;background:rgba(38,47,49,.28);z-index:60;pointer-events:none}
432
+ .m-top{display:flex;align-items:center;gap:9px;height:48px;padding:0 10px;background:linear-gradient(to bottom, rgba(12,175,125,.06), rgba(12,175,125,.015)),#F9F7F4;border-bottom:1px solid rgba(38,47,49,.12);flex:none}
433
+ .m-burger{width:40px;height:40px;border:none;background:none;border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--ink);cursor:pointer;flex:none;margin-left:-6px}
434
+ .m-burger:hover{background:var(--side-hover)}
435
+ .m-burger svg{width:18px;height:18px}
436
+ .m-tile{width:24px;height:24px;border-radius:6px;background:#fff;border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:11px;color:#C0392B;flex:none}
437
+ .m-name{font-size:13px;font-weight:700;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
438
+ .m-iconbtn{width:36px;height:36px;border:none;background:none;border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--muted);cursor:pointer;flex:none}
439
+ .m-iconbtn:hover{background:var(--side-hover);color:var(--ink)}
440
+ .m-iconbtn svg{width:15px;height:15px}
441
+ .m-av{width:28px;height:28px;border-radius:50%;background:var(--accent-tint);color:var(--accent-deep);font-size:10.5px;font-weight:700;display:flex;align-items:center;justify-content:center;flex:none}
442
+ .m-scroll{flex:1;overflow-y:auto;padding:14px 12px 70px}
443
+ .m-scrim{position:absolute;inset:0;background:rgba(38,47,49,.32);backdrop-filter:blur(1px);opacity:0;pointer-events:none;transition:opacity .2s;z-index:20}
444
+ .ph.nav-open .m-scrim,.ph.sheet-open .m-scrim{opacity:1;pointer-events:auto}
445
+ .m-drawer{position:absolute;top:0;bottom:0;left:0;width:272px;background:#F9F7F4;border-right:1px solid var(--border);transform:translateX(-105%);transition:transform .2s cubic-bezier(.2,0,0,1);z-index:30;display:flex;flex-direction:column}
446
+ .ph.nav-open .m-drawer{transform:none}
447
+ .m-drawer-head{display:flex;align-items:center;gap:9px;padding:12px 12px;border-bottom:1px solid var(--border)}
448
+ .m-drawer-head .x{margin-left:auto;width:36px;height:36px;border:none;background:none;border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--subtle);cursor:pointer}
449
+ .m-drawer-head .x:hover{background:var(--side-hover);color:var(--ink)}
450
+ .m-drawer-head .x svg{width:15px;height:15px}
451
+ .m-drawer nav{flex:1;overflow-y:auto;padding:8px}
452
+ .m-drawer nav a{display:flex;align-items:center;gap:10px;height:44px;padding:0 12px;border-radius:8px;font-size:13.5px;font-weight:500;color:var(--muted);text-decoration:none}
453
+ .m-drawer nav a:hover{background:var(--side-hover);color:var(--ink)}
454
+ .m-drawer nav a.on{background:var(--accent-tint);color:var(--accent-deep);font-weight:600}
455
+ .m-drawer nav a svg{width:16px;height:16px;flex:none}
456
+ .m-kpis{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:10px}
457
+ .m-kpi{background:#fff;border:1px solid var(--border);border-radius:10px;box-shadow:var(--edge);padding:11px 12px}
458
+ .m-kpi .k{font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--subtle)}
459
+ .m-kpi .v{font-size:17px;font-weight:700;letter-spacing:-.01em;margin:3px 0 4px;font-variant-numeric:tabular-nums}
460
+ .m-chiprow{display:flex;gap:2px;overflow-x:auto;margin:10px 0;padding-bottom:2px;scrollbar-width:none}
461
+ .m-chiprow::-webkit-scrollbar{display:none}
462
+ .m-chiprow .b-chip{flex:none;height:34px}
463
+ .m-row{display:flex;align-items:center;gap:10px;padding:12px 12px;border-bottom:1px solid var(--border);background:#fff}
464
+ .m-row:last-child{border-bottom:none}
465
+ .m-row b{font-size:13px;display:block}
466
+ .m-row .s{font-size:11.5px;color:var(--subtle)}
467
+ .m-row .amt{margin-left:auto;text-align:right;font-variant-numeric:tabular-nums;font-size:13px;font-weight:600;flex:none}
468
+ .m-savebar{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;gap:8px;padding:9px 12px 22px;background:linear-gradient(to top, rgba(232,162,19,.08), rgba(232,162,19,.015)),#F9F7F4;border-top:1px solid rgba(38,47,49,.12);z-index:15}
469
+ .m-savebar .dot{width:8px;height:8px;border-radius:50%;background:var(--warn);flex:none}
470
+ .m-savebar .lbl{font-size:12.5px;font-weight:700}
471
+ .m-savebar .sp{flex:1}
472
+ .m-savebar .b-btn{height:36px}
473
+ .m-sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:16px 16px 0 0;transform:translateY(105%);transition:transform .22s cubic-bezier(.2,0,0,1);z-index:40;box-shadow:var(--lift);padding:8px 18px 30px}
474
+ .ph.sheet-open .m-sheet{transform:none}
475
+ .m-sheet .grab{width:36px;height:4px;border-radius:999px;background:var(--border-strong);margin:4px auto 12px}
476
+ .m-sheet h4{font-size:15px;font-weight:700;margin-bottom:5px}
477
+ .m-sheet p{font-size:12.5px;color:var(--muted);margin-bottom:16px}
478
+ .m-sheet .acts{display:flex;justify-content:flex-end;gap:8px}
479
+ .m-sheet .b-btn{height:40px}
480
+ .m-toast{position:absolute;left:12px;right:12px;bottom:70px;display:flex;align-items:center;gap:8px;background:linear-gradient(to top, rgba(18,155,108,.10), rgba(18,155,108,.02)),#fff;border:1px solid var(--border);border-radius:8px;padding:11px 13px;font-size:12.5px;font-weight:600;box-shadow:var(--pop);z-index:16}
481
+ .m-toast svg{width:14px;height:14px;color:#0B7A57;flex:none}
482
+
483
+ /* ── pattern switcher ─────────────────────────────────── */
484
+ .ptabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:18px}
485
+ .ptab{border:1px solid var(--pg-border);background:var(--pg-card);color:var(--pg-muted);font:inherit;font-size:12.5px;font-weight:600;padding:8px 15px;border-radius:999px;cursor:pointer;transition:all .14s}
486
+ .ptab:hover{color:var(--pg-ink)}
487
+ .ptab.on{background:var(--pg-ink);color:var(--pg-bg);border-color:var(--pg-ink)}
488
+ .ptab .k{opacity:.55;font-weight:700;margin-right:6px;font-size:11px}
489
+ .artwrap{display:none}
490
+ .artwrap.on{display:block}
491
+ .art-caption{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;margin-bottom:6px}
492
+ .art-caption span{font-size:12px;color:var(--pg-muted);background:var(--pg-card);border:1px solid var(--pg-border);border-radius:999px;padding:4px 12px}
493
+ .art-caption span b{color:var(--pg-ink);font-weight:600}
494
+
495
+ /* mini board strip for primitives */
496
+ .strip{background:#F7F3ED;border:1px solid var(--frame-edge);border-radius:12px;padding:20px;display:flex;flex-wrap:wrap;gap:14px;align-items:center}
497
+ .strip + .strip{margin-top:10px}
498
+ .strip .lab{width:100%;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#8C9899;margin-bottom:-2px}
499
+
500
+ footer{padding:48px 0 64px;color:var(--pg-subtle);font-size:12.5px}
501
+ footer .wrap{border-top:1px solid var(--pg-border);padding-top:24px}
502
+
503
+ /* charts */
504
+ .b-chartgrid line{stroke:var(--border);stroke-dasharray:3 4;stroke-width:1}
505
+ .b-axis{font-size:10.5px;fill:var(--subtle);font-family:inherit}
506
+ </style>
507
+
508
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
509
+ <defs>
510
+ <symbol id="i-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5"/><path d="M5.5 8.8V21h13V8.8"/><path d="M9.5 21v-6h5v6"/></symbol>
511
+ <symbol id="i-receipt" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 3h13v18l-2.2-1.4-2.1 1.4-2.2-1.4L9.9 21l-2.2-1.4L5.5 21z"/><path d="M9 8.5h6M9 12.5h6"/></symbol>
512
+ <symbol id="i-box" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8.2 12 3 3 8.2v7.6L12 21l9-5.2z"/><path d="M3 8.2l9 5.1 9-5.1"/><path d="M12 21v-7.7"/></symbol>
513
+ <symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.4"/><path d="M2.8 20c.4-3.3 3-5.2 6.2-5.2s5.8 1.9 6.2 5.2"/><path d="M15.5 5.2a3.2 3.2 0 0 1 0 6"/><path d="M17.3 15.1c2.4.5 4 2.2 4.3 4.9"/></symbol>
514
+ <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5A2.5 2.5 0 0 1 5.5 5h4l2 2.5h7A2.5 2.5 0 0 1 21 10v7.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z"/></symbol>
515
+ <symbol id="i-tag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11.8 11.8 3H21v9.2L12.2 21z"/><circle cx="16.2" cy="7.8" r="1.3"/></symbol>
516
+ <symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21.3S5.2 15 5.2 10.2a6.8 6.8 0 0 1 13.6 0C18.8 15 12 21.3 12 21.3z"/><circle cx="12" cy="10" r="2.4"/></symbol>
517
+ <symbol id="i-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.4" cy="9.6" r="1.5"/><path d="M21 15.5 16 10.5 6.5 19"/></symbol>
518
+ <symbol id="i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3.5 7.5 8.5 6 8.5-6"/></symbol>
519
+ <symbol id="i-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20v-7M12 20V5M19 20v-10"/></symbol>
520
+ <symbol id="i-card" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="5.5" width="19" height="13" rx="2"/><path d="M2.5 10h19"/></symbol>
521
+ <symbol id="i-truck" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6.5H14V17H2.5z"/><path d="M14 9.5h3.8l2.7 3V17H14"/><circle cx="7" cy="17.6" r="1.9"/><circle cx="16.8" cy="17.6" r="1.9"/></symbol>
522
+ <symbol id="i-pct" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19 19 5"/><circle cx="7" cy="7" r="2.4"/><circle cx="17" cy="17" r="2.4"/></symbol>
523
+ <symbol id="i-sliders" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M4 12h16M4 17h16"/><circle cx="9.5" cy="7" r="1.9" fill="var(--sidebar,#F9F7F4)"/><circle cx="15" cy="12" r="1.9" fill="var(--sidebar,#F9F7F4)"/><circle cx="7.5" cy="17" r="1.9" fill="var(--sidebar,#F9F7F4)"/></symbol>
524
+ <symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6.4"/><path d="M20.5 20.5 15.8 15.8"/></symbol>
525
+ <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></symbol>
526
+ <symbol id="i-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
527
+ <symbol id="i-back" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M11 18l-6-6 6-6"/></symbol>
528
+ <symbol id="i-more" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></symbol>
529
+ <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 12.5 5 5 10-11"/></symbol>
530
+ <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>
531
+ <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9.5 7V4.5h5V7M6.5 7l.9 13.5h9.2L17.5 7"/><path d="M10 11v6M14 11v6"/></symbol>
532
+ <symbol id="i-ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4 10.5 13.5M17.5 13.5V20h-13V6.5H11"/></symbol>
533
+ <symbol id="i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 19.5.9-3.6L16.6 4.7a2 2 0 0 1 2.8 2.8L8.2 18.7z"/></symbol>
534
+ <symbol id="i-store" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.2 4h15.6L21 9H3z"/><path d="M5 9v11h14V9"/><path d="M9.5 20v-5.5h5V20"/></symbol>
535
+ <symbol id="i-dup" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 4H6a2 2 0 0 0-2 2v10"/></symbol>
536
+ </defs>
537
+ </svg>
538
+
539
+ <header class="hero">
540
+ <div class="wrap">
541
+ <div class="eyebrow">Rovela Admin · Design Language Proposal</div>
542
+ <h1>LEDGER<br>Warm paper. Precise ink. One emerald.</h1>
543
+ <p class="lede">One visual universe for the store admin, built from the platform's own DNA. Every screen is composed from the same twelve primitives under eight laws, so the product feels engineered rather than assembled, and the CSS gets lighter with every page we migrate.</p>
544
+ <div class="meta">
545
+ <span>13px working size</span><span>Weight over size</span><span>1px edges, no shadow soup</span><span>Accent used once per view</span><span>140ms ease-out</span><span>Tabular numerals</span>
546
+ </div>
547
+ </div>
548
+ </header>
549
+
550
+ <nav class="toc" aria-label="Sections">
551
+ <div class="wrap">
552
+ <a href="#laws">The 8 laws</a>
553
+ <a href="#foundations">Foundations</a>
554
+ <a href="#primitives">Primitives</a>
555
+ <a href="#patterns">Page patterns</a>
556
+ <a href="#lighter">Lighter code</a>
557
+ </div>
558
+ </nav>
559
+
560
+ <!-- ═══════════════════════ LAWS ═══════════════════════ -->
561
+ <section id="laws">
562
+ <div class="wrap">
563
+ <div class="sec-eyebrow">Section 1</div>
564
+ <h2 class="sec-title">Eight laws every component follows</h2>
565
+ <p class="sec-note">These are the whole rulebook. Anything a component wants to do visually is an application of one of these laws; anything outside them is a bug. They live as a comment block at the top of admin-theme.css and in CLAUDE.md so Claude Code obeys them when it builds custom admin pages.</p>
566
+ <div class="laws">
567
+ <div class="law"><div class="n">LAW 01</div><h3>13px is the working size</h3><p>Body, tables, forms and nav all read at 13px. Hierarchy below 16px is carried by weight (500 → 600 → 700), never by size. 20px exists for exactly two things: the page title and hero stat values.</p></div>
568
+ <div class="law"><div class="n">LAW 02</div><h3>The emerald is spent once</h3><p>Per view, the accent appears on the single primary action, the active nav item and focus rings. Nothing else. Links, icons, filters and avatars are ink or tint, so the one green button is unmistakably "the thing to do".</p></div>
569
+ <div class="law"><div class="n">LAW 03</div><h3>One status recipe</h3><p>Every state chip is a tinted background with a dark saturated foreground at 4.5:1. Five tones: success, warning, error, info, attention. No solid fills, no alpha tricks, no borrowing the accent.</p></div>
570
+ <div class="law"><div class="n">LAW 04</div><h3>Edges, not shadows</h3><p>Resting cards sit on the cream with a hairline border and a 1px bottom edge. Blur is earned by elevation: popovers get a soft 4px, modals a deep 16px. Nothing else floats.</p></div>
571
+ <div class="law"><div class="n">LAW 05</div><h3>Differentiated radii</h3><p>6px on controls, 10px on cards, 12px on modals, full round on badges and chips. One radius everywhere is how templates look; three radii is how products look.</p></div>
572
+ <div class="law"><div class="n">LAW 06</div><h3>Numbers are typeset</h3><p>Money and quantities render in tabular numerals, right-aligned, so columns never wobble. Order numbers, SKUs and codes render in the mono stack. A commerce tool is judged by how its numbers sit.</p></div>
573
+ <div class="law"><div class="n">LAW 07</div><h3>Icons are quiet</h3><p>Lucide only, 15 to 16px beside 13px text, 1.5px stroke. Icons appear in nav, buttons and empty states. They never decorate data cells: no envelope before every email, no calendar before every date.</p></div>
574
+ <div class="law"><div class="n">LAW 08</div><h3>Motion comes from somewhere</h3><p>One token pair: 140ms ease-out for micro feedback, 200ms for surfaces. Menus grow from their trigger, the save bar slides from the bottom, toasts rise. Nothing fades in from nowhere, and reduced-motion is respected.</p></div>
575
+ </div>
576
+ </div>
577
+ </section>
578
+
579
+ <!-- ═══════════════════════ FOUNDATIONS ═══════════════════════ -->
580
+ <section id="foundations">
581
+ <div class="wrap">
582
+ <div class="sec-eyebrow">Section 2</div>
583
+ <h2 class="sec-title">Foundations</h2>
584
+ <p class="sec-note">The palette is the platform's: warm cream ground, white cards, warm ink, one emerald. These are the exact tokens already landed in admin-theme.css; nothing here invents a second world.</p>
585
+
586
+ <div class="panel">
587
+ <h3>Color · surfaces &amp; ink</h3>
588
+ <div class="swatches">
589
+ <div class="sw"><div class="chip" style="background:#F7F3ED"></div><div class="lbl"><b>Cream ground</b><span>#F7F3ED</span></div></div>
590
+ <div class="sw"><div class="chip" style="background:#FFFFFF;border-bottom:1px solid #E5E0D6"></div><div class="lbl"><b>Card</b><span>#FFFFFF</span></div></div>
591
+ <div class="sw"><div class="chip" style="background:#F9F7F4"></div><div class="lbl"><b>Sidebar</b><span>#F9F7F4</span></div></div>
592
+ <div class="sw"><div class="chip" style="background:#E5E0D6"></div><div class="lbl"><b>Hairline</b><span>#E5E0D6</span></div></div>
593
+ <div class="sw"><div class="chip" style="background:#262F31"></div><div class="lbl"><b>Ink</b><span>#262F31</span></div></div>
594
+ <div class="sw"><div class="chip" style="background:#657172"></div><div class="lbl"><b>Ink muted</b><span>#657172</span></div></div>
595
+ <div class="sw"><div class="chip" style="background:#0CAF7D"></div><div class="lbl"><b>Emerald (scarce)</b><span>#0CAF7D</span></div></div>
596
+ <div class="sw"><div class="chip" style="background:#087C58"></div><div class="lbl"><b>Emerald deep</b><span>#087C58 on tints</span></div></div>
597
+ </div>
598
+ </div>
599
+
600
+ <div class="cols cols-2">
601
+ <div class="panel">
602
+ <h3>Color · the one status recipe</h3>
603
+ <div style="display:flex;flex-direction:column;gap:10px">
604
+ <div style="display:flex;align-items:center;gap:12px"><span class="b-badge" style="background:#E2F5EC;color:#0B7A57">Paid</span><span style="font-size:12px;color:var(--pg-muted)">success · #E2F5EC / #0B7A57</span></div>
605
+ <div style="display:flex;align-items:center;gap:12px"><span class="b-badge" style="background:#FCF0D8;color:#8A6116">Pending</span><span style="font-size:12px;color:var(--pg-muted)">warning · #FCF0D8 / #8A6116</span></div>
606
+ <div style="display:flex;align-items:center;gap:12px"><span class="b-badge" style="background:#FCE5E3;color:#B3372C">Out of stock</span><span style="font-size:12px;color:var(--pg-muted)">error · #FCE5E3 / #B3372C</span></div>
607
+ <div style="display:flex;align-items:center;gap:12px"><span class="b-badge" style="background:#E3EFFA;color:#2563A8">Shipped</span><span style="font-size:12px;color:var(--pg-muted)">info · #E3EFFA / #2563A8</span></div>
608
+ <div style="display:flex;align-items:center;gap:12px"><span class="b-badge" style="background:#EEF2D9;color:#5F6E1D">Needs review</span><span style="font-size:12px;color:var(--pg-muted)">attention · #EEF2D9 / #5F6E1D</span></div>
609
+ <div style="display:flex;align-items:center;gap:12px"><span class="b-badge" style="background:#EFEBE4;color:#657172">Cancelled</span><span style="font-size:12px;color:var(--pg-muted)">neutral · never black, never red</span></div>
610
+ </div>
611
+ </div>
612
+ <div class="panel">
613
+ <h3>Type · the inverted ramp (Plus Jakarta Sans)</h3>
614
+ <div class="type-row"><span class="px">20 / 700 / -1%</span><span style="font-size:20px;font-weight:700;letter-spacing:-0.01em">Orders</span><span class="use">Page titles and hero stats. The only large text.</span></div>
615
+ <div class="type-row"><span class="px">14 / 700</span><span style="font-size:14px;font-weight:700">Customer details</span><span class="use">Card and section titles</span></div>
616
+ <div class="type-row"><span class="px">13 / 400–600</span><span style="font-size:13px">The working size: tables, forms, nav, body</span><span class="use">Weight makes hierarchy, not size</span></div>
617
+ <div class="type-row"><span class="px">12 / 500</span><span style="font-size:12px;color:var(--pg-muted)">Helper text, meta, timestamps</span><span class="use">Secondary voice</span></div>
618
+ <div class="type-row"><span class="px">11 / 700 / +7%</span><span style="font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--pg-subtle)">Performance</span><span class="use">Eyebrows, column headers, KPI labels</span></div>
619
+ <div class="type-row"><span class="px">12 mono</span><span class="code" style="font-size:12px">#DD71DFDD · SKU 100638</span><span class="use">IDs, SKUs, codes</span></div>
620
+ </div>
621
+ </div>
622
+
623
+ <div class="cols cols-3">
624
+ <div class="panel">
625
+ <h3>Radii</h3>
626
+ <div style="display:flex;gap:10px;align-items:flex-end">
627
+ <div style="text-align:center"><div style="width:64px;height:40px;border:1.5px solid var(--pg-border);border-radius:6px;background:var(--pg-bg)"></div><div style="font-size:11px;color:var(--pg-subtle);margin-top:6px" class="code">6 · controls</div></div>
628
+ <div style="text-align:center"><div style="width:64px;height:48px;border:1.5px solid var(--pg-border);border-radius:10px;background:var(--pg-bg)"></div><div style="font-size:11px;color:var(--pg-subtle);margin-top:6px" class="code">10 · cards</div></div>
629
+ <div style="text-align:center"><div style="width:64px;height:56px;border:1.5px solid var(--pg-border);border-radius:12px;background:var(--pg-bg)"></div><div style="font-size:11px;color:var(--pg-subtle);margin-top:6px" class="code">12 · modals</div></div>
630
+ <div style="text-align:center"><div style="width:64px;height:22px;border:1.5px solid var(--pg-border);border-radius:999px;background:var(--pg-bg)"></div><div style="font-size:11px;color:var(--pg-subtle);margin-top:6px" class="code">999 · chips</div></div>
631
+ </div>
632
+ </div>
633
+ <div class="panel">
634
+ <h3>Elevation · three levels only</h3>
635
+ <div style="display:flex;flex-direction:column;gap:8px;font-size:12.5px">
636
+ <div style="display:flex;justify-content:space-between;gap:12px"><b style="font-weight:600">Resting card</b><span class="code" style="font-size:11px;color:var(--pg-subtle)">1px border + 0 1px 2px @4%</span></div>
637
+ <div style="display:flex;justify-content:space-between;gap:12px"><b style="font-weight:600">Popover / menu</b><span class="code" style="font-size:11px;color:var(--pg-subtle)">0 4px 14px -4px @14%</span></div>
638
+ <div style="display:flex;justify-content:space-between;gap:12px"><b style="font-weight:600">Modal / save bar</b><span class="code" style="font-size:11px;color:var(--pg-subtle)">0 16px 44px -14px @28%</span></div>
639
+ </div>
640
+ </div>
641
+ <div class="panel">
642
+ <h3>Motion · one token pair</h3>
643
+ <div style="display:flex;flex-direction:column;gap:8px;font-size:12.5px">
644
+ <div style="display:flex;justify-content:space-between;gap:12px"><b style="font-weight:600">Micro (hover, chips)</b><span class="code" style="font-size:11px;color:var(--pg-subtle)">140ms cubic-bezier(.2,0,0,1)</span></div>
645
+ <div style="display:flex;justify-content:space-between;gap:12px"><b style="font-weight:600">Surface (bar, modal, drawer)</b><span class="code" style="font-size:11px;color:var(--pg-subtle)">200ms, same curve</span></div>
646
+ <div style="display:flex;justify-content:space-between;gap:12px"><b style="font-weight:600">Origin rule</b><span style="font-size:11px;color:var(--pg-subtle)">everything enters from its trigger</span></div>
647
+ </div>
648
+ </div>
649
+ </div>
650
+ </div>
651
+ </section>
652
+
653
+ <!-- ═══════════════════════ PRIMITIVES ═══════════════════════ -->
654
+ <section id="primitives">
655
+ <div class="wrap">
656
+ <div class="sec-eyebrow">Section 3</div>
657
+ <h2 class="sec-title">Twelve primitives, rendered live</h2>
658
+ <p class="sec-note">Everything below is real HTML and CSS following the laws, on the actual cream ground. Hover them. These twelve are the entire component vocabulary: header, button, input, select, badge, chip, table, row menu, save bar, modal, empty state, skeleton. Every admin page is a composition of these and nothing else.</p>
659
+
660
+ <div class="strip board">
661
+ <div class="lab">Buttons · one primary per view, secondaries are ink</div>
662
+ <button class="b-btn b-btn-primary"><svg><use href="#i-plus"/></svg>Add product</button>
663
+ <button class="b-btn b-btn-secondary"><svg><use href="#i-ext"/></svg>View store</button>
664
+ <button class="b-btn b-btn-secondary">Export</button>
665
+ <button class="b-btn b-btn-ghost">Cancel</button>
666
+ <button class="b-btn b-btn-danger">Delete</button>
667
+ <button class="b-btn b-btn-icon" aria-label="More actions"><svg><use href="#i-more"/></svg></button>
668
+ <span style="flex-basis:100%"></span>
669
+ <span style="font-size:11.5px;color:#8C9899">Danger is quiet until confirmed; the solid red button lives only inside the confirm modal.</span>
670
+ </div>
671
+
672
+ <div class="strip board">
673
+ <div class="lab">Inputs &amp; selects · never browser-native, one focus ring</div>
674
+ <label class="b-input" style="max-width:250px"><svg><use href="#i-search"/></svg><input placeholder="Search orders" aria-label="Search orders"></label>
675
+ <span class="b-select" role="button" tabindex="0">All statuses<svg><use href="#i-chev"/></svg></span>
676
+ <span class="b-select" role="button" tabindex="0">Newest first<svg><use href="#i-chev"/></svg></span>
677
+ <span class="b-seg"><button>Today</button><button>7d</button><button class="on">30d</button><button>90d</button></span>
678
+ <span class="b-kbd">⌘K</span>
679
+ </div>
680
+
681
+ <div class="strip board">
682
+ <div class="lab">Badges &amp; chips · status speaks in tints, filters in emerald tint</div>
683
+ <span class="b-badge b-badge-ok">Paid</span>
684
+ <span class="b-badge b-badge-info">Shipped</span>
685
+ <span class="b-badge b-badge-warn">Pending</span>
686
+ <span class="b-badge b-badge-err">Out of stock</span>
687
+ <span class="b-badge b-badge-attn">Needs review</span>
688
+ <span class="b-badge b-badge-mute">Draft</span>
689
+ <span style="width:18px"></span>
690
+ <button class="b-chip">All</button>
691
+ <button class="b-chip on">Active</button>
692
+ <button class="b-chip">Drafts</button>
693
+ <button class="b-chip">Archived</button>
694
+ </div>
695
+
696
+ <div class="strip board" style="display:block">
697
+ <div class="lab" style="margin-bottom:12px">Page header · one component, four slots: back, eyebrow + title, subtitle, actions</div>
698
+ <div class="b-phead" style="margin-bottom:0">
699
+ <button class="back" aria-label="Back to products"><svg><use href="#i-back"/></svg></button>
700
+ <div class="tt">
701
+ <div class="eyebrow">Products</div>
702
+ <h1>Ampoule LED bulbe bleu B22 <span class="b-badge b-badge-ok" style="position:relative;top:-1px">Active</span></h1>
703
+ <div class="sub">SKU 100638 · last edited 2 hours ago</div>
704
+ </div>
705
+ <div class="acts">
706
+ <button class="b-btn b-btn-icon" aria-label="More"><svg><use href="#i-more"/></svg></button>
707
+ <button class="b-btn b-btn-secondary"><svg><use href="#i-ext"/></svg>Preview</button>
708
+ <button class="b-btn b-btn-primary">Save</button>
709
+ </div>
710
+ </div>
711
+ </div>
712
+
713
+ <div class="strip board" style="display:block">
714
+ <div class="lab" style="margin-bottom:12px">Empty state &amp; skeleton · designed, specific, geometry-true</div>
715
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
716
+ <div class="b-card"><div class="b-empty">
717
+ <div class="ic"><svg><use href="#i-tag"/></svg></div>
718
+ <b>No discounts yet</b>
719
+ <p>Create a code or an automatic promotion. It applies in your checkout with card and manual payments alike.</p>
720
+ <button class="b-btn b-btn-primary b-btn-sm"><svg><use href="#i-plus"/></svg>Create discount</button>
721
+ </div></div>
722
+ <div class="b-card b-card-pad">
723
+ <div style="display:flex;flex-direction:column;gap:14px;padding-top:4px">
724
+ <div style="display:flex;gap:12px;align-items:center"><div class="b-skel" style="width:34px;height:34px;border-radius:8px"></div><div style="flex:1"><div class="b-skel" style="width:55%;margin-bottom:8px"></div><div class="b-skel" style="width:35%"></div></div><div class="b-skel" style="width:56px;height:20px;border-radius:999px"></div></div>
725
+ <div style="display:flex;gap:12px;align-items:center"><div class="b-skel" style="width:34px;height:34px;border-radius:8px"></div><div style="flex:1"><div class="b-skel" style="width:65%;margin-bottom:8px"></div><div class="b-skel" style="width:30%"></div></div><div class="b-skel" style="width:56px;height:20px;border-radius:999px"></div></div>
726
+ <div style="display:flex;gap:12px;align-items:center"><div class="b-skel" style="width:34px;height:34px;border-radius:8px"></div><div style="flex:1"><div class="b-skel" style="width:45%;margin-bottom:8px"></div><div class="b-skel" style="width:40%"></div></div><div class="b-skel" style="width:56px;height:20px;border-radius:999px"></div></div>
727
+ </div>
728
+ <div style="font-size:11.5px;color:#8C9899;margin-top:14px">Skeletons mirror the final row geometry. Spinners exist only inside buttons.</div>
729
+ </div>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </section>
734
+
735
+ <!-- ═══════════════════════ PATTERNS ═══════════════════════ -->
736
+ <section id="patterns">
737
+ <div class="wrap">
738
+ <div class="sec-eyebrow">Section 4</div>
739
+ <h2 class="sec-title">The five page patterns</h2>
740
+ <p class="sec-note">Every admin route is one of five shapes. These artboards are interactive: check rows on Orders to raise the bulk bar, edit a field on the Product page to summon the save bar, open the row menu on Discounts and delete one to see the canonical confirm and toast. Tab 07 is the consolidation kit from the July 11 full-admin audit: for every job that still has several answers in the code, the one sanctioned answer.</p>
741
+
742
+ <div class="ptabs" role="tablist">
743
+ <button class="ptab on" data-art="art-dash" role="tab"><span class="k">01</span>Dashboard</button>
744
+ <button class="ptab" data-art="art-index" role="tab"><span class="k">02</span>Index · Orders</button>
745
+ <button class="ptab" data-art="art-detail" role="tab"><span class="k">03</span>Detail · Product</button>
746
+ <button class="ptab" data-art="art-mgr" role="tab"><span class="k">04</span>Manager · Discounts</button>
747
+ <button class="ptab" data-art="art-states" role="tab"><span class="k">05</span>Overlays &amp; states</button>
748
+ <button class="ptab" data-art="art-auth" role="tab"><span class="k">06</span>Auth</button>
749
+ <button class="ptab" data-art="art-onesource" role="tab"><span class="k">07</span>One way each</button>
750
+ <button class="ptab" data-art="art-mobile" role="tab"><span class="k">08</span>Mobile</button>
751
+ </div>
752
+
753
+ <!-- ── ARTBOARD 01 · DASHBOARD ── -->
754
+ <div class="artwrap on" id="art-dash">
755
+ <div class="frame">
756
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">easyelec.rovela.app/admin</span></div>
757
+ <div class="frame-scroll"><div class="board b-shell">
758
+ <aside class="b-side">
759
+ <div class="b-side-brand"><div class="logo">E</div><div><b>EasyElec</b><span>Admin</span></div></div>
760
+ <nav class="b-side-nav">
761
+ <a class="on" href="#art-dash" onclick="return false"><svg><use href="#i-home"/></svg>Home</a>
762
+ <a href="#" onclick="return false"><svg><use href="#i-receipt"/></svg>Orders</a>
763
+ <a href="#" onclick="return false"><svg><use href="#i-box"/></svg>Products</a>
764
+ <a href="#" onclick="return false"><svg><use href="#i-users"/></svg>Customers</a>
765
+ <div class="b-side-eyebrow">Store</div>
766
+ <a href="#" onclick="return false"><svg><use href="#i-folder"/></svg>Categories</a>
767
+ <a href="#" onclick="return false"><svg><use href="#i-tag"/></svg>Discounts</a>
768
+ <a href="#" onclick="return false"><svg><use href="#i-pin"/></svg>Locations</a>
769
+ <a href="#" onclick="return false"><svg><use href="#i-image"/></svg>Files</a>
770
+ <a href="#" onclick="return false"><svg><use href="#i-mail"/></svg>Emails</a>
771
+ <a href="#" onclick="return false"><svg><use href="#i-chart"/></svg>Analytics</a>
772
+ <div class="b-side-eyebrow">Configuration</div>
773
+ <a href="#" onclick="return false"><svg><use href="#i-card"/></svg>Payments</a>
774
+ <a href="#" onclick="return false"><svg><use href="#i-truck"/></svg>Shipping</a>
775
+ <a href="#" onclick="return false"><svg><use href="#i-pct"/></svg>Tax</a>
776
+ <a href="#" onclick="return false"><svg><use href="#i-users"/></svg>Team</a>
777
+ <a href="#" onclick="return false"><svg><use href="#i-sliders"/></svg>Settings</a>
778
+ </nav>
779
+ <div class="b-side-foot">
780
+ <a href="#" onclick="return false" style="display:flex;align-items:center;gap:9px;height:30px;padding:0 10px;border-radius:6px;font-size:12.5px;font-weight:500;color:var(--muted);text-decoration:none"><svg style="width:14px;height:14px"><use href="#i-edit"/></svg>Edit store</a>
781
+ <a href="#" onclick="return false" style="display:flex;align-items:center;gap:9px;height:30px;padding:0 10px;border-radius:6px;font-size:12.5px;font-weight:500;color:var(--muted);text-decoration:none"><svg style="width:14px;height:14px"><use href="#i-ext"/></svg>View store</a>
782
+ <div class="b-side-user"><div class="av">GB</div><div><b>Ghali Bennis</b></div><svg style="width:13px;height:13px;color:var(--subtle);margin-left:auto"><use href="#i-chev"/></svg></div>
783
+ </div>
784
+ </aside>
785
+ <main class="b-main">
786
+ <div class="b-phead">
787
+ <div class="tt">
788
+ <div class="eyebrow">Friday, July 10</div>
789
+ <h1>Good afternoon, Ghali</h1>
790
+ </div>
791
+ <div class="acts">
792
+ <span class="b-seg"><button>Today</button><button>7d</button><button class="on">30d</button><button>90d</button></span>
793
+ <button class="b-btn b-btn-secondary"><svg><use href="#i-ext"/></svg>View store</button>
794
+ <button class="b-btn b-btn-primary"><svg><use href="#i-plus"/></svg>Add product</button>
795
+ </div>
796
+ </div>
797
+
798
+ <div class="b-kpis">
799
+ <div class="b-kpi"><div class="k">Revenue</div><div class="v num">€4,090.00</div><div class="m"><span class="b-delta up"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 17 17 7M9 7h8v8"/></svg>5.1%</span>vs Jun 10 – Jul 9</div></div>
800
+ <div class="b-kpi"><div class="k">Orders</div><div class="v num">15</div><div class="m"><span class="b-delta up"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 17 17 7M9 7h8v8"/></svg>87.5%</span>vs prior period</div></div>
801
+ <div class="b-kpi"><div class="k">Average order</div><div class="v num">€272.63</div><div class="m"><span class="b-delta down"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 7l10 10M17 9v8H9"/></svg>44%</span>vs prior period</div></div>
802
+ <div class="b-kpi"><div class="k">Refund rate</div><div class="v num">0%</div><div class="m"><span class="b-delta flat">–</span>no refunds this period</div></div>
803
+ </div>
804
+
805
+ <div style="display:grid;grid-template-columns:1fr 320px;gap:12px;margin-bottom:12px">
806
+ <div class="b-card b-card-pad">
807
+ <div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:8px">
808
+ <div><div class="b-card-title">Revenue</div><div class="b-card-sub">Daily, last 30 days</div></div>
809
+ <div style="text-align:right"><span class="num" style="font-size:20px;font-weight:700;letter-spacing:-0.01em">€4,090.00</span><div class="b-card-sub">total</div></div>
810
+ </div>
811
+ <svg viewBox="0 0 640 170" style="width:100%;height:auto;display:block" role="img" aria-label="Revenue line chart">
812
+ <g class="b-chartgrid">
813
+ <line x1="0" y1="20" x2="640" y2="20"/><line x1="0" y1="60" x2="640" y2="60"/><line x1="0" y1="100" x2="640" y2="100"/><line x1="0" y1="140" x2="640" y2="140"/>
814
+ </g>
815
+ <text class="b-axis" x="0" y="14">€1.8K</text><text class="b-axis" x="0" y="96">€900</text><text class="b-axis" x="0" y="136">€450</text>
816
+ <path d="M40 148 L90 138 L140 142 L190 120 L240 132 L290 128 L340 136 L390 118 L440 126 L490 30 L540 118 L600 112 L600 152 L40 152 Z" fill="#0CAF7D" opacity="0.07"/>
817
+ <path d="M40 148 L90 138 L140 142 L190 120 L240 132 L290 128 L340 136 L390 118 L440 126 L490 30 L540 118 L600 112" fill="none" stroke="#0CAF7D" stroke-width="1.75" stroke-linejoin="round" stroke-linecap="round"/>
818
+ <circle cx="600" cy="112" r="3.5" fill="#0CAF7D"/>
819
+ <circle cx="600" cy="112" r="7" fill="#0CAF7D" opacity=".18"/>
820
+ <text class="b-axis" x="36" y="166">Jun 11</text><text class="b-axis" x="300" y="166">Jun 25</text><text class="b-axis" x="572" y="166">Jul 9</text>
821
+ </svg>
822
+ </div>
823
+ <div class="b-card b-card-pad">
824
+ <div class="b-card-title">Order status</div>
825
+ <div class="b-card-sub" style="margin-bottom:14px">20 orders in period</div>
826
+ <div style="display:flex;height:8px;border-radius:999px;overflow:hidden;margin-bottom:16px">
827
+ <span style="width:50%;background:#129B6C"></span><span style="width:15%;background:#E8A213"></span><span style="width:15%;background:#3372BD"></span><span style="width:10%;background:#0B7A57"></span><span style="width:10%;background:#C9C1B1"></span>
828
+ </div>
829
+ <div style="display:flex;flex-direction:column;gap:9px;font-size:12.5px">
830
+ <div style="display:flex;align-items:center;gap:8px"><i style="width:8px;height:8px;border-radius:2px;background:#129B6C"></i>Paid<span class="num" style="margin-left:auto;font-weight:600">10</span><span class="num" style="color:var(--subtle);width:36px;text-align:right">50%</span></div>
831
+ <div style="display:flex;align-items:center;gap:8px"><i style="width:8px;height:8px;border-radius:2px;background:#E8A213"></i>Pending<span class="num" style="margin-left:auto;font-weight:600">3</span><span class="num" style="color:var(--subtle);width:36px;text-align:right">15%</span></div>
832
+ <div style="display:flex;align-items:center;gap:8px"><i style="width:8px;height:8px;border-radius:2px;background:#3372BD"></i>Shipped<span class="num" style="margin-left:auto;font-weight:600">3</span><span class="num" style="color:var(--subtle);width:36px;text-align:right">15%</span></div>
833
+ <div style="display:flex;align-items:center;gap:8px"><i style="width:8px;height:8px;border-radius:2px;background:#0B7A57"></i>Delivered<span class="num" style="margin-left:auto;font-weight:600">2</span><span class="num" style="color:var(--subtle);width:36px;text-align:right">10%</span></div>
834
+ <div style="display:flex;align-items:center;gap:8px"><i style="width:8px;height:8px;border-radius:2px;background:#C9C1B1"></i>Cancelled<span class="num" style="margin-left:auto;font-weight:600">2</span><span class="num" style="color:var(--subtle);width:36px;text-align:right">10%</span></div>
835
+ </div>
836
+ </div>
837
+ </div>
838
+
839
+ <div class="b-card">
840
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border)">
841
+ <div class="b-card-title" style="margin:0">Recent orders</div>
842
+ <button class="b-btn b-btn-ghost b-btn-sm">View all</button>
843
+ </div>
844
+ <table class="b-table">
845
+ <thead><tr><th>Order</th><th>Customer</th><th>Status</th><th class="r">Total</th><th class="r">Date</th></tr></thead>
846
+ <tbody>
847
+ <tr><td class="mono">#DD71DFDD</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-info">Shipped</span></td><td class="r num" style="font-weight:600">€537.56</td><td class="r muted num">9 Jul, 10:40</td></tr>
848
+ <tr><td class="mono">#BD95B061</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-ok">Delivered</span></td><td class="r num" style="font-weight:600">€987.73</td><td class="r muted num">9 Jul, 10:24</td></tr>
849
+ <tr><td class="mono">#5DCEEBD5</td><td>romain.berthier@gmail.com</td><td><span class="b-badge b-badge-ok">Paid</span></td><td class="r num" style="font-weight:600">€40.41</td><td class="r muted num">9 Jul, 10:13</td></tr>
850
+ </tbody>
851
+ </table>
852
+ </div>
853
+ </main>
854
+ </div></div>
855
+ </div>
856
+ <div class="art-caption">
857
+ <span><b>What changed:</b> icon chips removed from KPI cards, numbers own the card</span>
858
+ <span>tabular numerals throughout</span>
859
+ <span>donut → segmented bar, cancelled is neutral, never black</span>
860
+ <span>one emerald button in the whole view</span>
861
+ <span>chart: hairline grid, emphasized endpoint</span>
862
+ </div>
863
+ </div>
864
+
865
+ <!-- ── ARTBOARD 02 · INDEX (ORDERS) ── -->
866
+ <div class="artwrap" id="art-index">
867
+ <div class="frame">
868
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">easyelec.rovela.app/admin/orders</span></div>
869
+ <div class="frame-scroll"><div class="board b-shell">
870
+ <main class="b-main" style="padding:22px 28px 30px">
871
+ <div class="b-phead">
872
+ <div class="tt">
873
+ <h1>Orders</h1>
874
+ <div class="sub">15 orders this period · <span class="num">€4,090.00</span> collected</div>
875
+ </div>
876
+ <div class="acts">
877
+ <button class="b-btn b-btn-secondary">Export</button>
878
+ </div>
879
+ </div>
880
+
881
+ <div class="b-listrow" id="orders-list">
882
+ <div class="b-filters">
883
+ <label class="b-input" style="max-width:240px"><svg><use href="#i-search"/></svg><input placeholder="Search order or email" aria-label="Search orders"></label>
884
+ <button class="b-chip on">All</button>
885
+ <button class="b-chip">To fulfill <span class="num" style="margin-left:5px;opacity:.65">6</span></button>
886
+ <button class="b-chip">Pending <span class="num" style="margin-left:5px;opacity:.65">3</span></button>
887
+ <button class="b-chip">Completed</button>
888
+ <span class="grow"></span>
889
+ <span class="b-select" role="button" tabindex="0">Newest first<svg><use href="#i-chev"/></svg></span>
890
+ </div>
891
+ <div class="b-bulk" role="toolbar" aria-label="Bulk actions">
892
+ <span id="bulk-count" class="num">2 selected</span>
893
+ <button class="b-btn b-btn-sm">Mark as shipped</button>
894
+ <button class="b-btn b-btn-sm">Print packing slips</button>
895
+ <span class="sp"></span>
896
+ <span class="x" id="bulk-clear" role="button" tabindex="0" aria-label="Clear selection"><svg style="width:14px;height:14px"><use href="#i-x"/></svg></span>
897
+ </div>
898
+
899
+ <div class="b-card" style="overflow:visible">
900
+ <table class="b-table">
901
+ <thead><tr>
902
+ <th style="width:36px"><input type="checkbox" class="b-check" id="check-all" aria-label="Select all"></th>
903
+ <th>Order</th><th>Customer</th><th>Status</th><th class="r">Total</th><th class="r">Date</th><th style="width:44px"></th>
904
+ </tr></thead>
905
+ <tbody>
906
+ <tr><td><input type="checkbox" class="b-check row-check" aria-label="Select order"></td><td class="mono">#DD71DFDD</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-info">Shipped</span></td><td class="r num" style="font-weight:600">€537.56</td><td class="r muted num">9 Jul, 10:40</td>
907
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Order actions"><svg><use href="#i-more"/></svg></button>
908
+ <span class="b-menu"><button><svg><use href="#i-receipt"/></svg>View order</button><button><svg><use href="#i-check"/></svg>Mark as delivered</button><button><svg><use href="#i-mail"/></svg>Resend confirmation</button><hr><button class="danger"><svg><use href="#i-x"/></svg>Cancel order</button></span>
909
+ </span></td></tr>
910
+ <tr><td><input type="checkbox" class="b-check row-check" aria-label="Select order"></td><td class="mono">#E3644758</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-warn">Pending</span></td><td class="r num" style="font-weight:600">€617.28</td><td class="r muted num">8 Jul, 19:24</td>
911
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Order actions"><svg><use href="#i-more"/></svg></button>
912
+ <span class="b-menu"><button><svg><use href="#i-receipt"/></svg>View order</button><button><svg><use href="#i-check"/></svg>Mark as paid</button><button><svg><use href="#i-mail"/></svg>Resend confirmation</button><hr><button class="danger"><svg><use href="#i-x"/></svg>Cancel order</button></span>
913
+ </span></td></tr>
914
+ <tr><td><input type="checkbox" class="b-check row-check" aria-label="Select order"></td><td class="mono">#C08DAA98</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-ok">Paid</span></td><td class="r num" style="font-weight:600">€1,214.57</td><td class="r muted num">8 Jul, 19:09</td>
915
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Order actions"><svg><use href="#i-more"/></svg></button>
916
+ <span class="b-menu"><button><svg><use href="#i-receipt"/></svg>View order</button><button><svg><use href="#i-truck"/></svg>Mark as shipped</button><button><svg><use href="#i-mail"/></svg>Resend confirmation</button><hr><button class="danger"><svg><use href="#i-x"/></svg>Cancel order</button></span>
917
+ </span></td></tr>
918
+ <tr><td><input type="checkbox" class="b-check row-check" aria-label="Select order"></td><td class="mono">#FAEAB4C4</td><td>info@enitherm.com</td><td><span class="b-badge b-badge-warn">Pending</span></td><td class="r num" style="font-weight:600">€2,191.81</td><td class="r muted num">29 Jun, 16:33</td>
919
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Order actions"><svg><use href="#i-more"/></svg></button>
920
+ <span class="b-menu"><button><svg><use href="#i-receipt"/></svg>View order</button><button><svg><use href="#i-check"/></svg>Mark as paid</button><button><svg><use href="#i-mail"/></svg>Resend confirmation</button><hr><button class="danger"><svg><use href="#i-x"/></svg>Cancel order</button></span>
921
+ </span></td></tr>
922
+ <tr><td><input type="checkbox" class="b-check row-check" aria-label="Select order"></td><td class="mono">#3155279C</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-info">Shipped</span></td><td class="r num" style="font-weight:600">€64.10</td><td class="r muted num">5 Jul, 16:52</td>
923
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Order actions"><svg><use href="#i-more"/></svg></button>
924
+ <span class="b-menu"><button><svg><use href="#i-receipt"/></svg>View order</button><button><svg><use href="#i-check"/></svg>Mark as delivered</button><button><svg><use href="#i-mail"/></svg>Resend confirmation</button><hr><button class="danger"><svg><use href="#i-x"/></svg>Cancel order</button></span>
925
+ </span></td></tr>
926
+ <tr><td><input type="checkbox" class="b-check row-check" aria-label="Select order"></td><td class="mono">#992A832B</td><td>ghali@rovela.ai</td><td><span class="b-badge b-badge-ok">Paid</span></td><td class="r num" style="font-weight:600">€48.70</td><td class="r muted num">7 Jul, 16:52</td>
927
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Order actions"><svg><use href="#i-more"/></svg></button>
928
+ <span class="b-menu"><button><svg><use href="#i-receipt"/></svg>View order</button><button><svg><use href="#i-truck"/></svg>Mark as shipped</button><button><svg><use href="#i-mail"/></svg>Resend confirmation</button><hr><button class="danger"><svg><use href="#i-x"/></svg>Cancel order</button></span>
929
+ </span></td></tr>
930
+ </tbody>
931
+ </table>
932
+ </div>
933
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-top:12px;font-size:12px;color:var(--subtle)">
934
+ <span class="num">1–6 of 15</span>
935
+ <span style="display:flex;gap:6px"><button class="b-btn b-btn-secondary b-btn-sm" disabled style="opacity:.5;cursor:default">Previous</button><button class="b-btn b-btn-secondary b-btn-sm">Next</button></span>
936
+ </div>
937
+ </div>
938
+ </main>
939
+ </div></div>
940
+ </div>
941
+ <div class="art-caption">
942
+ <span><b>What changed:</b> one filter row, always: search + view chips + sort</span>
943
+ <span>per-row status dropdowns → badge + row menu</span>
944
+ <span>select rows: the filter row becomes the bulk bar in place, try it</span>
945
+ <span>order numbers in mono, totals tabular right-aligned</span>
946
+ <span>the row itself is the link</span>
947
+ </div>
948
+ </div>
949
+
950
+ <!-- ── ARTBOARD 03 · DETAIL (PRODUCT) ── -->
951
+ <div class="artwrap" id="art-detail">
952
+ <div class="frame">
953
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">easyelec.rovela.app/admin/products/100638</span></div>
954
+ <div class="frame-scroll"><div class="board b-shell" id="detail-board">
955
+ <main class="b-main">
956
+ <div class="b-phead">
957
+ <button class="back" aria-label="Back to products"><svg><use href="#i-back"/></svg></button>
958
+ <div class="tt">
959
+ <div class="eyebrow">Products</div>
960
+ <h1>Ampoule LED bulbe bleu B22 1W <span class="b-badge b-badge-ok" style="position:relative;top:-1px">Active</span></h1>
961
+ <div class="sub">SKU 100638 · 5 sold in the last 30 days</div>
962
+ </div>
963
+ <div class="acts">
964
+ <button class="b-btn b-btn-icon" aria-label="More actions"><svg><use href="#i-more"/></svg></button>
965
+ <button class="b-btn b-btn-secondary"><svg><use href="#i-ext"/></svg>Preview</button>
966
+ </div>
967
+ </div>
968
+
969
+ <div style="display:grid;grid-template-columns:1fr 300px;gap:12px;align-items:start">
970
+ <div style="display:flex;flex-direction:column;gap:12px">
971
+ <div class="b-card b-card-pad">
972
+ <div class="b-card-title" style="margin-bottom:14px">Details</div>
973
+ <div class="b-field">
974
+ <label class="b-label" for="f-name">Name</label>
975
+ <label class="b-input"><input id="f-name" class="dirty-watch" value="Ampoule LED bulbe bleu B22 1W 230Vac"></label>
976
+ </div>
977
+ <div class="b-field">
978
+ <label class="b-label" for="f-desc">Description</label>
979
+ <textarea id="f-desc" class="b-input dirty-watch" rows="3">Ampoule LED B22 basse consommation, teinte bleue, 1W, 230Vac. Idéale pour guirlandes et enseignes.</textarea>
980
+ <div class="b-help">Shown on the product page. Type here to see the save bar.</div>
981
+ </div>
982
+ </div>
983
+ <div class="b-card b-card-pad">
984
+ <div class="b-card-title" style="margin-bottom:14px">Pricing</div>
985
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
986
+ <div><label class="b-label" for="f-price">Price</label><label class="b-input"><input id="f-price" class="dirty-watch num" value="€11.00"></label></div>
987
+ <div><label class="b-label" for="f-cmp">Compare-at</label><label class="b-input"><input id="f-cmp" class="dirty-watch num" value="€14.00"></label></div>
988
+ <div><label class="b-label" for="f-sku">SKU</label><label class="b-input"><input id="f-sku" class="dirty-watch mono" value="100638"></label></div>
989
+ </div>
990
+ </div>
991
+ <div class="b-card b-card-pad">
992
+ <div class="b-card-title" style="margin-bottom:12px">Media</div>
993
+ <div style="border:1.5px dashed var(--border-strong);border-radius:8px;padding:26px;display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--subtle)">
994
+ <svg style="width:20px;height:20px"><use href="#i-image"/></svg>
995
+ <span style="font-size:12.5px;font-weight:600;color:var(--muted)">Drop images or videos, or browse</span>
996
+ <span style="font-size:11.5px">Up to 10 items · <a href="#" onclick="return false" style="color:var(--accent-deep);font-weight:600;text-decoration:none">choose from library</a></span>
997
+ </div>
998
+ </div>
999
+ </div>
1000
+ <div style="display:flex;flex-direction:column;gap:12px">
1001
+ <div class="b-card b-card-pad">
1002
+ <div class="b-card-title" style="margin-bottom:12px">Status</div>
1003
+ <span class="b-select" style="width:100%" role="button" tabindex="0">Active<svg><use href="#i-chev"/></svg></span>
1004
+ <div class="b-help">Visible in your storefront and search.</div>
1005
+ </div>
1006
+ <div class="b-card b-card-pad">
1007
+ <div class="b-card-title" style="margin-bottom:12px">Organization</div>
1008
+ <div class="b-field"><label class="b-label">Category</label><span class="b-select" style="width:100%" role="button" tabindex="0">Ampoules LED<svg><use href="#i-chev"/></svg></span></div>
1009
+ <div class="b-field"><label class="b-label">Vendor</label><label class="b-input"><input class="dirty-watch" value="Vision-EL"></label></div>
1010
+ </div>
1011
+ <div class="b-card b-card-pad">
1012
+ <div class="b-card-title" style="margin-bottom:10px">Inventory</div>
1013
+ <div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px"><span class="muted" style="color:var(--muted)">In stock</span><b class="num">42</b></div>
1014
+ <div style="display:flex;justify-content:space-between;font-size:13px"><span style="color:var(--muted)">Sold, 30 days</span><b class="num">5</b></div>
1015
+ </div>
1016
+ </div>
1017
+ </div>
1018
+
1019
+ <div class="b-savebar" id="savebar">
1020
+ <span class="dot"></span>
1021
+ <span class="lbl" id="sb-label">Unsaved changes</span>
1022
+ <span class="meta" id="sb-meta"></span>
1023
+ <span class="sp"></span>
1024
+ <span class="b-kbd">⌘S</span>
1025
+ <button class="b-btn b-btn-ghost" id="discard-btn">Discard</button>
1026
+ <button class="b-btn b-btn-primary" id="save-btn">Save</button>
1027
+ </div>
1028
+ </main>
1029
+ </div></div>
1030
+ </div>
1031
+ <div class="art-caption">
1032
+ <span><b>What changed:</b> back button belongs to the header band, framed and consistent</span>
1033
+ <span>2/3 editing + 1/3 status &amp; metadata, capped width</span>
1034
+ <span><b>the attention band:</b> edit any field — the amber bar docks to the bottom edge with the changed fields named, then morphs Saving → Saved on save</span>
1035
+ <span>replaces every floating pill and per-card Save on document pages</span>
1036
+ <span>selects are styled, never browser-native</span>
1037
+ </div>
1038
+ </div>
1039
+
1040
+ <!-- ── ARTBOARD 04 · MANAGER (DISCOUNTS) ── -->
1041
+ <div class="artwrap" id="art-mgr">
1042
+ <div class="frame">
1043
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">easyelec.rovela.app/admin/discounts</span></div>
1044
+ <div class="frame-scroll"><div class="board b-shell" id="mgr-board">
1045
+ <main class="b-main">
1046
+ <div class="b-phead">
1047
+ <div class="tt">
1048
+ <h1>Discounts</h1>
1049
+ <div class="sub">Codes and automatic promotions, applied in your checkout</div>
1050
+ </div>
1051
+ <div class="acts">
1052
+ <button class="b-btn b-btn-primary"><svg><use href="#i-plus"/></svg>Create discount</button>
1053
+ </div>
1054
+ </div>
1055
+
1056
+ <div class="b-card">
1057
+ <table class="b-table">
1058
+ <thead><tr><th>Discount</th><th>Type</th><th>Status</th><th class="r">Used</th><th style="width:44px"></th></tr></thead>
1059
+ <tbody>
1060
+ <tr>
1061
+ <td><span class="mono" style="font-weight:600">WELCOME10</span><div style="font-size:12px;color:var(--muted);white-space:normal">10% off entire order · Offre de bienvenue</div></td>
1062
+ <td class="muted">Code</td>
1063
+ <td><span class="b-badge b-badge-ok">Active</span></td>
1064
+ <td class="r num">1</td>
1065
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Discount actions"><svg><use href="#i-more"/></svg></button>
1066
+ <span class="b-menu"><button><svg><use href="#i-edit"/></svg>Edit</button><button><svg><use href="#i-dup"/></svg>Duplicate</button><button><svg><use href="#i-x"/></svg>Deactivate</button><hr><button class="danger del-discount" data-code="WELCOME10"><svg><use href="#i-trash"/></svg>Delete</button></span>
1067
+ </span></td>
1068
+ </tr>
1069
+ <tr>
1070
+ <td><span class="mono" style="font-weight:600">PRO15</span><div style="font-size:12px;color:var(--muted);white-space:normal">15% off entire order · Offre pro</div></td>
1071
+ <td class="muted">Code · <span class="b-badge b-badge-attn" style="height:18px;font-size:11px">Group-only</span></td>
1072
+ <td><span class="b-badge b-badge-ok">Active</span></td>
1073
+ <td class="r num">0</td>
1074
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Discount actions"><svg><use href="#i-more"/></svg></button>
1075
+ <span class="b-menu"><button><svg><use href="#i-edit"/></svg>Edit</button><button><svg><use href="#i-dup"/></svg>Duplicate</button><button><svg><use href="#i-x"/></svg>Deactivate</button><hr><button class="danger del-discount" data-code="PRO15"><svg><use href="#i-trash"/></svg>Delete</button></span>
1076
+ </span></td>
1077
+ </tr>
1078
+ <tr>
1079
+ <td><span style="font-weight:600">10% dès 500 € d'achat</span><div style="font-size:12px;color:var(--muted);white-space:normal">10% off entire order, applies automatically</div></td>
1080
+ <td class="muted">Automatic</td>
1081
+ <td><span class="b-badge b-badge-ok">Active</span></td>
1082
+ <td class="r num">2</td>
1083
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Discount actions"><svg><use href="#i-more"/></svg></button>
1084
+ <span class="b-menu"><button><svg><use href="#i-edit"/></svg>Edit</button><button><svg><use href="#i-dup"/></svg>Duplicate</button><button><svg><use href="#i-x"/></svg>Deactivate</button><hr><button class="danger del-discount" data-code="this automatic promotion"><svg><use href="#i-trash"/></svg>Delete</button></span>
1085
+ </span></td>
1086
+ </tr>
1087
+ <tr>
1088
+ <td><span class="mono" style="font-weight:600">LIVRAISON</span><div style="font-size:12px;color:var(--muted);white-space:normal">Free shipping · Livraison offerte</div></td>
1089
+ <td class="muted">Code</td>
1090
+ <td><span class="b-badge b-badge-mute">Inactive</span></td>
1091
+ <td class="r num">0</td>
1092
+ <td class="r"><span class="b-menu-wrap"><button class="b-btn b-btn-icon row-menu-btn" aria-label="Discount actions"><svg><use href="#i-more"/></svg></button>
1093
+ <span class="b-menu"><button><svg><use href="#i-edit"/></svg>Edit</button><button><svg><use href="#i-dup"/></svg>Duplicate</button><button><svg><use href="#i-check"/></svg>Activate</button><hr><button class="danger del-discount" data-code="LIVRAISON"><svg><use href="#i-trash"/></svg>Delete</button></span>
1094
+ </span></td>
1095
+ </tr>
1096
+ </tbody>
1097
+ </table>
1098
+ </div>
1099
+
1100
+ <div class="b-overlay" id="mgr-overlay">
1101
+ <div class="b-modal" role="dialog" aria-modal="true" aria-labelledby="del-title">
1102
+ <h4 id="del-title">Delete discount?</h4>
1103
+ <p><b id="del-code">WELCOME10</b> will stop working immediately. Customers who already used it keep their orders. This can't be undone.</p>
1104
+ <div class="acts">
1105
+ <button class="b-btn b-btn-ghost" id="del-cancel">Cancel</button>
1106
+ <button class="b-btn b-btn-danger-solid" id="del-confirm">Delete discount</button>
1107
+ </div>
1108
+ </div>
1109
+ </div>
1110
+ <div class="b-toast" id="mgr-toast"><svg><use href="#i-check"/></svg>Discount deleted</div>
1111
+ </main>
1112
+ </div></div>
1113
+ </div>
1114
+ <div class="art-caption">
1115
+ <span><b>What changed:</b> the page header owns the title and the Create button, no second card header</span>
1116
+ <span>rows follow the index grammar: name + tinted status + numbers + row menu</span>
1117
+ <span>delete lives behind the menu and always goes through the one confirm modal, try it</span>
1118
+ </div>
1119
+ </div>
1120
+
1121
+ <!-- ── ARTBOARD 05 · OVERLAYS & STATES ── -->
1122
+ <div class="artwrap" id="art-states">
1123
+ <div class="frame">
1124
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">overlay &amp; feedback grammar</span></div>
1125
+ <div class="frame-scroll"><div class="board" style="min-width:1080px;padding:28px;display:grid;grid-template-columns:1fr 1fr;gap:14px;position:relative">
1126
+ <div class="b-toaster" id="toaster"></div>
1127
+ <div>
1128
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:10px">The one confirm modal · destructive</div>
1129
+ <div class="b-modal" style="opacity:1;transform:none;width:100%">
1130
+ <h4>Delete 3 products?</h4>
1131
+ <p>They will be removed from your storefront immediately. Products with order history are archived instead, so past orders stay intact.</p>
1132
+ <div class="acts"><button class="b-btn b-btn-ghost">Cancel</button><button class="b-btn b-btn-danger-solid">Delete products</button></div>
1133
+ </div>
1134
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin:18px 0 10px">The same modal · consequential, not destructive</div>
1135
+ <div class="b-modal" style="opacity:1;transform:none;width:100%">
1136
+ <h4>Refund €537.56?</h4>
1137
+ <p>The full amount returns to the customer's card in 5 to 10 business days. The order moves to <b>Refunded</b>.</p>
1138
+ <div class="acts"><button class="b-btn b-btn-ghost">Cancel</button><button class="b-btn b-btn-primary">Refund order</button></div>
1139
+ </div>
1140
+ </div>
1141
+ <div>
1142
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:10px">Menus grow from their trigger</div>
1143
+ <div class="b-card" style="padding:14px;display:flex;justify-content:flex-end;position:relative;height:196px">
1144
+ <span class="b-menu-wrap"><button class="b-btn b-btn-icon" style="background:var(--side-hover);color:var(--ink)" aria-label="Example menu"><svg><use href="#i-more"/></svg></button>
1145
+ <span class="b-menu open" style="position:absolute">
1146
+ <button><svg><use href="#i-edit"/></svg>Edit</button>
1147
+ <button><svg><use href="#i-dup"/></svg>Duplicate</button>
1148
+ <button><svg><use href="#i-ext"/></svg>View in store</button>
1149
+ <hr>
1150
+ <button class="danger"><svg><use href="#i-trash"/></svg>Delete</button>
1151
+ </span>
1152
+ </span>
1153
+ </div>
1154
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin:18px 0 10px">Notifications · one system, events only</div>
1155
+ <div style="display:flex;flex-direction:column;gap:8px">
1156
+ <div class="b-toast show" style="position:static;transform:none;opacity:1;width:fit-content"><svg><use href="#i-check"/></svg>Discount deleted<span style="font-size:11px;color:var(--subtle);margin-left:2px">auto-dismisses · 3s</span></div>
1157
+ <div class="b-toast show" style="position:static;transform:none;opacity:1;width:fit-content"><svg><use href="#i-check"/></svg>Test email sent to you<span class="b-toast-action">View email</span></div>
1158
+ <div class="b-toast show err" style="position:static;transform:none;opacity:1;width:fit-content"><svg><use href="#i-x"/></svg>Couldn't send the invite. Check the email address and try again.<span class="b-toast-x"><svg style="width:12px;height:12px"><use href="#i-x"/></svg></span><span style="font-size:11px;color:var(--subtle);margin-left:2px">sticky until dismissed</span></div>
1159
+ </div>
1160
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin:16px 0 8px">Try the stack</div>
1161
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
1162
+ <button class="b-btn sec" id="fire-ok" style="background:#fff;border:1px solid #D8D1C3;height:30px;border-radius:6px;padding:0 12px;font-size:12.5px;font-weight:600;cursor:pointer">Fire a success</button>
1163
+ <button class="b-btn sec" id="fire-action" style="background:#fff;border:1px solid #D8D1C3;height:30px;border-radius:6px;padding:0 12px;font-size:12.5px;font-weight:600;cursor:pointer">Success with action</button>
1164
+ <button class="b-btn sec" id="fire-err" style="background:#fff;border:1px solid #D8D1C3;height:30px;border-radius:6px;padding:0 12px;font-size:12.5px;font-weight:600;cursor:pointer">Fire an error</button>
1165
+ </div>
1166
+ <p style="font-size:12px;color:var(--muted);margin-top:12px;line-height:1.55">
1167
+ <b style="color:var(--ink)">The grammar — events vs states:</b> an EVENT (something just happened: deleted, sent, marked paid, failed) is always a toast from the ONE global toaster: ink pill, bottom-right, stack of 3, rises above the save bar when both exist. A STATE (something is wrong here and stays wrong: fetch failed, invalid field, auth error) is an inline alert at the thing it describes, and never auto-dismisses. Document saves are neither: the save bar's morph is their feedback. Never two channels for one outcome. Success toasts speak the button's words and dismiss in 3s; error toasts are sticky with a ✕ and say what to fix.
1168
+ </p>
1169
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin:18px 0 10px">Save bar · the attention band, four states</div>
1170
+ <div style="display:flex;flex-direction:column;gap:8px">
1171
+ <div class="b-savebar show" style="position:static;transform:none;border:1px solid var(--border);border-radius:8px">
1172
+ <span class="dot"></span><span class="lbl">Unsaved changes</span><span class="meta">Name · Price</span><span class="sp"></span><span class="b-kbd">⌘S</span><button class="b-btn b-btn-ghost">Discard</button><button class="b-btn b-btn-primary">Save</button>
1173
+ </div>
1174
+ <div class="b-savebar show saving" style="position:static;transform:none;border:1px solid var(--border);border-radius:8px">
1175
+ <span class="dot"></span><span class="lbl">Saving…</span><span class="sp"></span><button class="b-btn b-btn-ghost" disabled style="opacity:.5">Discard</button><button class="b-btn b-btn-primary" disabled style="opacity:.7">Saving…</button>
1176
+ </div>
1177
+ <div class="b-savebar show saved" style="position:static;transform:none;border:1px solid var(--border);border-radius:8px">
1178
+ <span class="dot"></span><span class="lbl">Saved</span><span class="meta">All changes are live in your store</span><span class="sp"></span>
1179
+ </div>
1180
+ <div class="b-savebar show error-state" style="position:static;transform:none;border:1px solid var(--border);border-radius:8px">
1181
+ <span class="dot"></span><span class="lbl">Couldn't save</span><span class="meta">Check the price field and try again</span><span class="sp"></span><button class="b-btn b-btn-ghost">Discard</button><button class="b-btn b-btn-primary">Try again</button>
1182
+ </div>
1183
+ </div>
1184
+ <p style="font-size:12px;color:var(--muted);margin-top:10px;line-height:1.55">
1185
+ <b style="color:var(--ink)">The rules:</b> document pages (Settings, Tax, a product) raise the bar on dirty state and it docks full-width to the bottom edge — the top bar's bookend. Transactional editors inside managers (create a discount, add a location) keep their inline form buttons. Clicking away while dirty <b style="color:var(--ink)">shakes the bar</b> instead of losing work; ⌘S saves; the saved morph is the feedback, so no toast fires for a bar save.
1186
+ </p>
1187
+ </div>
1188
+ </div></div>
1189
+ </div>
1190
+ <div class="art-caption">
1191
+ <span><b>The confirmation rule:</b> destructive = solid red in the modal only; consequential = primary; both name the object and the consequence</span>
1192
+ <span><b>The notification rule:</b> events → the one toaster (fire the playground buttons); states → inline alerts that never auto-dismiss; document saves → the save bar morph. Never two channels for one outcome</span>
1193
+ <span>toasts speak the button's language: "Delete discount" → "Discount deleted"</span>
1194
+ <span>no browser confirm(), ever</span>
1195
+ </div>
1196
+ </div>
1197
+
1198
+ <!-- ── ARTBOARD 06 · AUTH ── -->
1199
+ <div class="artwrap" id="art-auth">
1200
+ <div class="frame">
1201
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">easyelec.rovela.app/admin/login</span></div>
1202
+ <div class="frame-scroll"><div class="board v4">
1203
+ <div class="v4-grid">
1204
+ <div class="v4-main">
1205
+ <div class="v4-brand">
1206
+ <div class="v4-tile"><span>e</span></div>
1207
+ <div class="nm"><b>EasyElec</b><i>Admin</i></div>
1208
+ </div>
1209
+ <div class="v4-card">
1210
+ <h1>Sign in</h1>
1211
+ <p class="sub">Manage EasyElec from its admin.</p>
1212
+ <div class="v4-alert" id="auth-alert">That password doesn't match. Try again or reset it below.</div>
1213
+ <div class="v4-field">
1214
+ <div class="v4-lblrow"><span class="v4-lbl">Email</span></div>
1215
+ <div class="v4-input"><input type="email" value="ghali@rovela.ai"></div>
1216
+ </div>
1217
+ <div class="v4-field">
1218
+ <div class="v4-lblrow"><span class="v4-lbl">Password</span><a class="v4-forgot" href="#" onclick="return false">Forgot password?</a></div>
1219
+ <div class="v4-input"><input type="password" id="auth-pw" value="hunter2222"><span class="v4-eye" id="auth-eye"><svg style="width:14px;height:14px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z"/><circle cx="12" cy="12" r="2.6"/></svg></span></div>
1220
+ </div>
1221
+ <label class="v4-remember"><input type="checkbox" class="b-check" checked> Keep me signed in for 30 days</label>
1222
+ <button class="v4-submit" id="auth-submit">Sign in</button>
1223
+ </div>
1224
+ <div class="v4-pow">Powered by <b>⟠ Rovela</b></div>
1225
+ </div>
1226
+ <aside class="v4-rail">
1227
+ <div class="v4-rail-label">The other four doors · same shell</div>
1228
+ <div class="v4-mini">
1229
+ <div class="cap">/admin/login · first run</div>
1230
+ <h2>Create your admin account</h2>
1231
+ <p class="s">You're the owner of EasyElec. Set a password to get started.</p>
1232
+ <div class="f"></div><div class="f"></div><div class="f"></div>
1233
+ <div class="b">Create account</div>
1234
+ </div>
1235
+ <div class="v4-mini">
1236
+ <div class="cap">/admin/forgot-password</div>
1237
+ <h2>Forgot your password?</h2>
1238
+ <p class="s">We'll email you a reset link.</p>
1239
+ <div class="f"></div>
1240
+ <div class="b">Send reset link</div>
1241
+ </div>
1242
+ <div class="v4-mini">
1243
+ <div class="cap">/admin/forgot-password · sent</div>
1244
+ <h2>Check your email</h2>
1245
+ <p class="s">If an admin account exists for that address, the link is on its way.</p>
1246
+ <div class="okwash"><svg style="width:13px;height:13px"><use href="#i-check"/></svg>Sent to g•••@rovela.ai</div>
1247
+ </div>
1248
+ <div class="v4-mini">
1249
+ <div class="cap">/admin/accept-invite</div>
1250
+ <h2>Join EasyElec's team</h2>
1251
+ <p class="s">Ghali invited you as a Manager. Set a password to accept.</p>
1252
+ <div class="f"></div><div class="f"></div>
1253
+ <div class="b">Accept invite</div>
1254
+ </div>
1255
+ </aside>
1256
+ </div>
1257
+ </div></div>
1258
+ </div>
1259
+ <div class="art-caption">
1260
+ <span><b>One shell, five doors:</b> identity wash at the top, favicon-first brand tile + store name + Admin chip, one 380px card, powered-by beneath</span>
1261
+ <span>tile resolution: favicon → logo → ink monogram — the exact AdminTopBar logic, shared</span>
1262
+ <span>titles are 20px sentence-case purposes, never "Set Up Admin Account"</span>
1263
+ <span><b>try it:</b> toggle the eye, then Sign in — loading, then the error STATE inline (auth errors are never toasts)</span>
1264
+ <span>the forgot link lives on the password label row</span>
1265
+ </div>
1266
+ </div>
1267
+
1268
+ <!-- ── ARTBOARD 07 · ONE WAY EACH ── -->
1269
+ <div class="artwrap" id="art-onesource">
1270
+ <div class="frame">
1271
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">the consolidation kit — one sanctioned answer per job</span></div>
1272
+ <div class="frame-scroll"><div class="board" style="min-width:1080px;padding:22px;display:flex;flex-direction:column;gap:14px;position:relative">
1273
+
1274
+ <!-- live modal demo overlay (covers the whole board) -->
1275
+ <div class="b-overlay" id="os-overlay">
1276
+ <div class="b-modal">
1277
+ <div style="display:flex;align-items:flex-start">
1278
+ <h4 style="flex:1">Cancel this order?</h4>
1279
+ <button class="b-btn b-btn-icon" id="os-modal-x" aria-label="Close" style="margin:-6px -8px 0 0"><svg><use href="#i-x"/></svg></button>
1280
+ </div>
1281
+ <p>Order <b>#1041</b> moves to Cancelled and the customer is notified by email. Stock returns to your inventory.</p>
1282
+ <div class="acts"><button class="b-btn b-btn-ghost" id="os-modal-cancel">Keep order</button><button class="b-btn b-btn-danger-solid" id="os-modal-go">Cancel order</button></div>
1283
+ </div>
1284
+ </div>
1285
+
1286
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
1287
+ <!-- AdminModal -->
1288
+ <div class="os-cell">
1289
+ <div class="os-lab">AdminModal · the one overlay <span class="fix">8 implementations → 1</span></div>
1290
+ <div class="b-modal" style="opacity:1;transform:none;width:100%;border:1px solid var(--border);box-shadow:var(--pop)">
1291
+ <div style="display:flex;align-items:flex-start">
1292
+ <h4 style="flex:1">Remove this payment method?</h4>
1293
+ <button class="b-btn b-btn-icon" aria-label="Close" style="margin:-6px -8px 0 0"><svg><use href="#i-x"/></svg></button>
1294
+ </div>
1295
+ <p>Customers will no longer see <b>Bank transfer</b> at checkout. Past orders keep their records.</p>
1296
+ <div class="os-alert">Couldn't remove it. Check your connection and try again.</div>
1297
+ <div class="acts"><button class="b-btn b-btn-ghost">Cancel</button><button class="b-btn b-btn-danger-solid">Remove method</button></div>
1298
+ </div>
1299
+ <div class="os-beh">
1300
+ <span><svg><use href="#i-check"/></svg>React portal</span>
1301
+ <span><svg><use href="#i-check"/></svg>Blurred backdrop</span>
1302
+ <span><svg><use href="#i-check"/></svg>Esc closes</span>
1303
+ <span><svg><use href="#i-check"/></svg>Click-outside, guarded while busy</span>
1304
+ <span><svg><use href="#i-check"/></svg>Scroll lock</span>
1305
+ <span><svg><use href="#i-check"/></svg>Focus stays inside</span>
1306
+ </div>
1307
+ <div class="os-note"><b>Three widths, one anatomy:</b> 400px confirms · 480px editors (Shipping, Tax) · 720px pickers (media library). Errors inside are the inline STATE, never a toast. DeleteConfirmDialog rebuilds on top of it, so its 13 consumers never move.</div>
1308
+ <div style="margin-top:12px"><button class="b-btn b-btn-secondary" id="os-modal-open">Open the live modal</button> <span style="font-size:11.5px;color:var(--subtle);margin-left:6px">then press Esc, or click the backdrop</span></div>
1309
+ </div>
1310
+
1311
+ <!-- AdminEmptyState -->
1312
+ <div class="os-cell">
1313
+ <div class="os-lab">AdminEmptyState · two meanings, two messages <span class="fix">33 sites · 11 shapes → 1</span></div>
1314
+ <span class="b-seg" id="os-empty-seg"><button class="on" data-em="em-first">First run</button><button data-em="em-nomatch">No matches</button></span>
1315
+ <div id="em-first" style="border:1px solid var(--border);border-radius:8px;margin-top:10px">
1316
+ <div class="b-empty">
1317
+ <div class="ic"><svg><use href="#i-box"/></svg></div>
1318
+ <b>No products yet</b>
1319
+ <p>Your catalog starts here. Add your first product and it goes live on your storefront.</p>
1320
+ <button class="b-btn b-btn-primary"><svg><use href="#i-plus"/></svg>Add product</button>
1321
+ </div>
1322
+ </div>
1323
+ <div id="em-nomatch" style="border:1px solid var(--border);border-radius:8px;margin-top:10px;display:none">
1324
+ <div class="b-empty">
1325
+ <div class="ic" style="background:#EFEBE4;color:var(--muted)"><svg><use href="#i-search"/></svg></div>
1326
+ <b>No products match</b>
1327
+ <p>Nothing matches “cable” with these filters. Try different terms or clear the filters.</p>
1328
+ <button class="b-btn b-btn-secondary">Clear filters</button>
1329
+ </div>
1330
+ </div>
1331
+ <div class="os-note"><b>The rule:</b> an empty list always answers three questions: what is this, why is it empty, what do I do next. First run invites creation; a filter miss offers relief. Today only 5 of 33 sites make that distinction and only 1 offers a next step. Compact tier for sub-panels stays a single quiet line: <i style="font-style:normal;color:var(--subtle)">“No customer groups yet. Create them from Customers → Manage groups.”</i></div>
1332
+ </div>
1333
+ </div>
1334
+
1335
+ <!-- List toolkit -->
1336
+ <div class="os-cell">
1337
+ <div class="os-lab">The list toolkit · search, chips, pagination, row actions <span class="fix">4 paginations → 1 · 3 search markups → 1 · 16 native selects → 0</span></div>
1338
+ <div class="b-filters">
1339
+ <label class="b-input" style="max-width:260px"><svg><use href="#i-search"/></svg><input id="os-search" placeholder="Search products…" aria-label="Search products"></label>
1340
+ <span id="os-chips" style="display:inline-flex;gap:2px"><button class="b-chip on">All</button><button class="b-chip">Active</button><button class="b-chip">Drafts</button><button class="b-chip">Out of stock</button></span>
1341
+ <span class="grow"></span>
1342
+ <span class="b-select">Newest first<svg><use href="#i-chev"/></svg></span>
1343
+ </div>
1344
+ <table class="b-table" id="os-list">
1345
+ <thead><tr><th style="width:26px"></th><th>Product</th><th>Status</th><th class="r">Inventory</th><th class="r">Price</th><th style="width:84px"></th></tr></thead>
1346
+ <tbody>
1347
+ <tr><td><input type="checkbox" class="b-check"></td><td><b>USB-C Hub 7-in-1</b></td><td><span class="b-badge b-badge-ok">Active</span></td><td class="r num">142</td><td class="r num">€49.90</td><td><span style="display:inline-flex;gap:6px"><button class="b-btn b-btn-icon" aria-label="Edit"><svg><use href="#i-edit"/></svg></button> <button class="b-btn b-btn-icon" aria-label="Delete" style="color:var(--err-fg)"><svg><use href="#i-trash"/></svg></button></span></td></tr>
1348
+ <tr><td><input type="checkbox" class="b-check"></td><td><b>Cat-6 Cable 3m</b></td><td><span class="b-badge b-badge-ok">Active</span></td><td class="r num">908</td><td class="r num">€12.50</td><td><span style="display:inline-flex;gap:6px"><button class="b-btn b-btn-icon" aria-label="Edit"><svg><use href="#i-edit"/></svg></button> <button class="b-btn b-btn-icon" aria-label="Delete" style="color:var(--err-fg)"><svg><use href="#i-trash"/></svg></button></span></td></tr>
1349
+ <tr><td><input type="checkbox" class="b-check"></td><td><b>Smart Plug Duo</b></td><td><span class="b-badge b-badge-mute">Draft</span></td><td class="r num">54</td><td class="r num">€34.00</td><td><span style="display:inline-flex;gap:6px"><button class="b-btn b-btn-icon" aria-label="Edit"><svg><use href="#i-edit"/></svg></button> <button class="b-btn b-btn-icon" aria-label="Delete" style="color:var(--err-fg)"><svg><use href="#i-trash"/></svg></button></span></td></tr>
1350
+ <tr><td><input type="checkbox" class="b-check"></td><td><b>HDMI 2.1 Cable</b></td><td><span class="b-badge b-badge-err">Out of stock</span></td><td class="r num">0</td><td class="r num">€19.90</td><td><span style="display:inline-flex;gap:6px"><button class="b-btn b-btn-icon" aria-label="Edit"><svg><use href="#i-edit"/></svg></button> <button class="b-btn b-btn-icon" aria-label="Delete" style="color:var(--err-fg)"><svg><use href="#i-trash"/></svg></button></span></td></tr>
1351
+ </tbody>
1352
+ </table>
1353
+ <div class="os-pagerow">
1354
+ <span class="info num">Showing 1–4 of 84,907</span>
1355
+ <span style="flex:1"></span>
1356
+ <button class="b-btn b-btn-secondary b-btn-sm" disabled style="opacity:.5">Previous</button>
1357
+ <button class="b-btn b-btn-secondary b-btn-sm">Next</button>
1358
+ </div>
1359
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-top:14px">
1360
+ <div style="border:1px solid var(--border);border-radius:8px;padding:12px">
1361
+ <div style="font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:8px">Table rows → quiet icon pair</div>
1362
+ <span style="display:inline-flex;gap:6px"><button class="b-btn b-btn-icon" aria-label="Edit"><svg><use href="#i-edit"/></svg></button> <button class="b-btn b-btn-icon" aria-label="Delete" style="color:var(--err-fg)"><svg><use href="#i-trash"/></svg></button></span>
1363
+ </div>
1364
+ <div style="border:1px solid var(--border);border-radius:8px;padding:12px">
1365
+ <div style="font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:8px">Card lists → text actions</div>
1366
+ <span style="font-size:12.5px;font-weight:600;color:var(--accent-deep);cursor:pointer">Edit</span><span style="color:var(--border-strong);margin:0 8px">·</span><span style="font-size:12.5px;font-weight:600;color:var(--err-fg);cursor:pointer">Delete</span>
1367
+ </div>
1368
+ <div style="border:1px solid var(--border);border-radius:8px;padding:12px">
1369
+ <div style="font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle);margin-bottom:8px">3+ actions → one ⋯ menu</div>
1370
+ <button class="b-btn b-btn-icon" aria-label="More actions"><svg><use href="#i-more"/></svg></button><span style="font-size:11.5px;color:var(--subtle);margin-left:6px">permission-gated menus only</span>
1371
+ </div>
1372
+ </div>
1373
+ <div class="os-note"><b>Type in the search</b> — it filters live. In the product it debounces 300ms, so one keystroke never equals one request (today Products and Orders fetch on every key). Chips are the one filter idiom; dropdown filters are always the styled select, never a native one.</div>
1374
+ </div>
1375
+
1376
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
1377
+ <!-- AdminSwitch + field anatomy -->
1378
+ <div class="os-cell">
1379
+ <div class="os-lab">AdminSwitch + the one field anatomy <span class="fix">3 switches → 1 · 5 caption styles → 1</span></div>
1380
+ <div style="display:flex;flex-direction:column;gap:10px;margin-bottom:16px">
1381
+ <label style="display:flex;align-items:center;gap:10px;font-size:13px;cursor:pointer"><button class="b-switch os-sw" role="switch" aria-checked="true" aria-label="Charge tax on this product"></button>Charge tax on this product</label>
1382
+ <label style="display:flex;align-items:center;gap:10px;font-size:13px;cursor:pointer"><button class="b-switch os-sw" role="switch" aria-checked="false" aria-label="Enable cash on delivery"></button>Enable cash on delivery</label>
1383
+ <label style="display:flex;align-items:center;gap:10px;font-size:13px;color:var(--subtle)"><button class="b-switch" role="switch" aria-checked="true" disabled aria-label="Stripe payments, always on"></button>Stripe payments <span style="font-size:11px">(managed by Rovela)</span></label>
1384
+ </div>
1385
+ <div class="os-note" style="margin:0 0 16px"><b>The rule:</b> a state gets a switch, in Settings AND in managers. Today settings pages use switches while every manager uses a checkbox for the identical Enabled toggle. Checkboxes only ever mean selection.</div>
1386
+ <div class="b-field">
1387
+ <label class="b-label">Price <span style="color:#B3372C">*</span></label>
1388
+ <div class="os-addon"><span class="pre">€</span><input value="49.90" aria-label="Price"></div>
1389
+ <div class="b-help">Charged at checkout. Compare-at shows as a strikethrough.</div>
1390
+ </div>
1391
+ <div class="b-field">
1392
+ <label class="b-label">Compare-at price</label>
1393
+ <label class="b-input" style="border-color:#B3372C;box-shadow:0 0 0 3px rgba(179,55,44,.10)"><input value="0.00" aria-label="Compare-at price"></label>
1394
+ <div style="font-size:12px;color:#B3372C;margin-top:5px">Enter a price above 0.</div>
1395
+ </div>
1396
+ <div class="os-note" style="margin-top:12px"><b>One anatomy everywhere:</b> label with the required mark, field, one helper-text class, one error-text class. Currency lives inside the field as a left addon, never in the label. Five forms currently hide which fields are required; 80+ captions are hand-styled five different ways.</div>
1397
+ </div>
1398
+
1399
+ <!-- Loading grammar -->
1400
+ <div class="os-cell">
1401
+ <div class="os-lab">Loading · one spinner, one skeleton, one rule <span class="fix">3 spinners → 1</span></div>
1402
+ <div style="display:flex;align-items:center;gap:18px;margin-bottom:14px">
1403
+ <span style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)"><span class="b-spin sm"></span>12</span>
1404
+ <span style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)"><span class="b-spin"></span>16</span>
1405
+ <span style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)"><span class="b-spin lg"></span>22 · the only three sizes</span>
1406
+ </div>
1407
+ <div style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px">
1408
+ <div class="b-skel" style="width:38%"></div>
1409
+ <div class="b-skel" style="width:86%"></div>
1410
+ <div class="b-skel" style="width:64%"></div>
1411
+ </div>
1412
+ <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
1413
+ <button class="b-btn b-btn-primary" id="os-save" style="min-width:96px;justify-content:center">Save</button>
1414
+ <span style="font-size:11.5px;color:var(--subtle)">press it — the one in-flight style: spinner + present-progressive label</span>
1415
+ </div>
1416
+ <div class="os-note"><b>The rule:</b> first visit renders skeletons in the real layout. A refetch keeps the content mounted — never blank a table you already showed (today most tables flash to skeletons on every background reload). Buttons in flight show the small spinner and a present-progressive label with a real ellipsis: “Saving…”, never “Saving...”. Charts use the same spinner as everything else.</div>
1417
+ </div>
1418
+ </div>
1419
+
1420
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
1421
+ <!-- Badge semantics -->
1422
+ <div class="os-cell">
1423
+ <div class="os-lab">Badges · one recipe, semantic tones <span class="fix">“Inactive” had 3 colors → 1</span></div>
1424
+ <div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px">
1425
+ <span class="b-badge b-badge-ok">Paid</span>
1426
+ <span class="b-badge b-badge-warn">Pending</span>
1427
+ <span class="b-badge b-badge-info">Shipped</span>
1428
+ <span class="b-badge b-badge-attn">Return requested</span>
1429
+ <span class="b-badge b-badge-err">Out of stock</span>
1430
+ <span class="b-badge b-badge-mute">Inactive</span>
1431
+ </div>
1432
+ <div class="os-vs">
1433
+ <div>“Inactive” = warning, neutral or draft, depending on the page</div><div>Inactive is always neutral</div>
1434
+ <div>Green “paid” badge borrowed for Pickup and Default chips</div><div>Positive capability uses the success tone, by name</div>
1435
+ <div>The status→color map pasted in 4 files</div><div>One shared statusBadgeClass() helper</div>
1436
+ </div>
1437
+ <div class="os-note"><b>Tones mean things:</b> green is money received or success, amber is waiting, red is stopped, blue is in motion, neutral is off. A badge never borrows a tone for its color.</div>
1438
+ </div>
1439
+
1440
+ <!-- Voice -->
1441
+ <div class="os-cell">
1442
+ <div class="os-lab">Microcopy · sentence case, real ellipsis <span class="fix">17 files of “...” → “…”</span></div>
1443
+ <div class="os-vs">
1444
+ <div>Create Zone</div><div>Create zone</div>
1445
+ <div>Process Refund</div><div>Process refund</div>
1446
+ <div>Admin Sign In</div><div>Sign in</div>
1447
+ <div>Sign Out</div><div>Sign out</div>
1448
+ <div>Deleting...</div><div>Deleting…</div>
1449
+ </div>
1450
+ <div style="margin-top:14px;border:1px solid var(--border);border-radius:8px;padding:12px">
1451
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle)">Shipping address</div>
1452
+ <div style="font-size:12px;color:var(--muted);margin-top:6px">The one section eyebrow — a single class replaces 22 hand-rolled variants (4 different weights, trackings and sizes today).</div>
1453
+ </div>
1454
+ <div class="os-note"><b>Icons:</b> 16px and quiet, 1.5 stroke set once for the whole admin instead of per icon — today most icons silently render at the heavier default. Every icon-only button carries an aria-label.</div>
1455
+ </div>
1456
+ </div>
1457
+
1458
+ </div></div>
1459
+ </div>
1460
+ <div class="art-caption">
1461
+ <span><b>The principle:</b> behaviors live in the primitive so pages can't forget them — Esc, scroll lock and focus work everywhere because they're written once</span>
1462
+ <span><b>try it:</b> open the live modal (Esc closes it) · toggle the empty state · type in the list search · flip the switches · press Save</span>
1463
+ <span>everything is additive: .admin-* classes never rename, old stores keep working</span>
1464
+ <span>zero data-flow changes — chrome only, exactly like the auth pass</span>
1465
+ </div>
1466
+ </div>
1467
+
1468
+ <!-- ── ARTBOARD 08 · MOBILE ── -->
1469
+ <div class="artwrap" id="art-mobile">
1470
+ <div class="frame">
1471
+ <div class="frame-bar"><div class="dots"><i></i><i></i><i></i></div><span class="url">easyelec.rovela.app/admin · 390px — the same LEDGER, thumb-sized</span></div>
1472
+ <div class="frame-scroll"><div class="board" style="min-width:1080px;padding:22px;display:flex;flex-direction:column;gap:14px">
1473
+
1474
+ <div style="display:flex;gap:14px;align-items:flex-start;justify-content:center;flex-wrap:wrap">
1475
+
1476
+ <!-- PHONE A · shell + drawer -->
1477
+ <div>
1478
+ <div class="ph" id="ph-a">
1479
+ <div class="ph-status"><span>9:41</span><i>●●● ▲ ▮</i></div>
1480
+ <div class="ph-body">
1481
+ <div class="m-top">
1482
+ <button class="m-burger" id="ph-a-burger" aria-label="Open menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></button>
1483
+ <span class="m-tile">E</span>
1484
+ <span class="m-name">EasyElec</span>
1485
+ <span style="flex:1"></span>
1486
+ <button class="m-iconbtn" aria-label="View store"><svg><use href="#i-ext"/></svg></button>
1487
+ <button class="m-iconbtn" aria-label="Edit store"><svg><use href="#i-edit"/></svg></button>
1488
+ <span class="m-av">GB</span>
1489
+ </div>
1490
+ <div class="m-scroll">
1491
+ <div style="font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--subtle)">Friday, July 11</div>
1492
+ <div style="font-size:18px;font-weight:700;letter-spacing:-.01em;margin:2px 0 12px">Good afternoon, Ghali</div>
1493
+ <div class="m-kpis">
1494
+ <div class="m-kpi"><div class="k">Revenue</div><div class="v">€4,090.00</div><span class="b-delta up" style="height:16px;font-size:10px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 17 17 7M9 7h8v8"/></svg>5.1%</span></div>
1495
+ <div class="m-kpi"><div class="k">Orders</div><div class="v">15</div><span class="b-delta up" style="height:16px;font-size:10px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 17 17 7M9 7h8v8"/></svg>87.5%</span></div>
1496
+ <div class="m-kpi"><div class="k">Average order</div><div class="v">€272.63</div><span class="b-delta down" style="height:16px;font-size:10px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M7 7l10 10M17 9v8H9"/></svg>44%</span></div>
1497
+ <div class="m-kpi"><div class="k">Refund rate</div><div class="v">0%</div><span class="b-delta flat" style="height:16px;font-size:10px">–</span></div>
1498
+ </div>
1499
+ <div class="b-card b-card-pad" style="padding:13px">
1500
+ <div class="b-card-title" style="font-size:13px">Revenue</div>
1501
+ <div class="b-card-sub">Daily, last 30 days</div>
1502
+ <svg viewBox="0 0 300 90" style="width:100%;display:block;margin-top:8px"><path d="M8 74 L38 68 L68 70 L98 58 L128 64 L158 62 L188 66 L218 56 L248 16 L292 54 L292 82 L8 82 Z" fill="#0CAF7D" opacity=".08"/><path d="M8 74 L38 68 L68 70 L98 58 L128 64 L158 62 L188 66 L218 56 L248 16 L292 54" fill="none" stroke="#0CAF7D" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
1503
+ </div>
1504
+ </div>
1505
+ <div class="m-scrim" data-close="ph-a"></div>
1506
+ <div class="m-drawer">
1507
+ <div class="m-drawer-head">
1508
+ <span class="m-tile">E</span>
1509
+ <div><b style="font-size:13px;display:block;line-height:1.2">EasyElec</b><span style="font-size:10px;color:var(--subtle)">Admin</span></div>
1510
+ <button class="x" data-close="ph-a" aria-label="Close menu"><svg><use href="#i-x"/></svg></button>
1511
+ </div>
1512
+ <nav>
1513
+ <a class="on" href="#" onclick="return false"><svg><use href="#i-home"/></svg>Home</a>
1514
+ <a href="#" onclick="return false"><svg><use href="#i-receipt"/></svg>Orders</a>
1515
+ <a href="#" onclick="return false"><svg><use href="#i-box"/></svg>Products</a>
1516
+ <a href="#" onclick="return false"><svg><use href="#i-users"/></svg>Customers</a>
1517
+ <a href="#" onclick="return false"><svg><use href="#i-tag"/></svg>Discounts</a>
1518
+ <a href="#" onclick="return false"><svg><use href="#i-mail"/></svg>Emails</a>
1519
+ <a href="#" onclick="return false"><svg><use href="#i-chart"/></svg>Analytics</a>
1520
+ <a href="#" onclick="return false"><svg><use href="#i-sliders"/></svg>Settings</a>
1521
+ </nav>
1522
+ </div>
1523
+ <div class="ph-home"></div>
1524
+ </div>
1525
+ </div>
1526
+ <p style="text-align:center;font-size:11.5px;color:var(--pg-muted);margin-top:8px"><b style="color:var(--pg-ink)">The shell.</b> Tap the burger — drawer slides over a scrim, 44px rows, closes on scrim / X / navigate.</p>
1527
+ </div>
1528
+
1529
+ <!-- PHONE B · index page -->
1530
+ <div>
1531
+ <div class="ph">
1532
+ <div class="ph-status"><span>9:41</span><i>●●● ▲ ▮</i></div>
1533
+ <div class="ph-body">
1534
+ <div class="m-top">
1535
+ <button class="m-burger" aria-label="Open menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></button>
1536
+ <span class="m-tile">E</span>
1537
+ <span class="m-name">EasyElec</span>
1538
+ <span style="flex:1"></span>
1539
+ <button class="m-iconbtn" aria-label="View store"><svg><use href="#i-ext"/></svg></button>
1540
+ <span class="m-av">GB</span>
1541
+ </div>
1542
+ <div class="m-scroll">
1543
+ <div style="display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:10px">
1544
+ <div style="font-size:18px;font-weight:700;letter-spacing:-.01em">Orders</div>
1545
+ <button class="b-btn b-btn-primary" style="height:40px"><svg><use href="#i-plus"/></svg>Add</button>
1546
+ </div>
1547
+ <label class="b-input" style="height:44px"><svg><use href="#i-search"/></svg><input placeholder="Search by order ID or email…"></label>
1548
+ <div class="m-chiprow"><button class="b-chip on">All</button><button class="b-chip">Pending</button><button class="b-chip">Paid</button><button class="b-chip">Shipped</button><button class="b-chip">Delivered</button></div>
1549
+ <div style="border:1px solid var(--border);border-radius:10px;overflow:hidden;background:#fff">
1550
+ <div class="m-row"><div><b>#1042</b><span class="s">Marie Dubois · Jul 11</span></div><span class="b-badge b-badge-warn" style="margin-left:6px">Pending</span><span class="amt">€89.90</span></div>
1551
+ <div class="m-row"><div><b>#1041</b><span class="s">Tom Verly · Jul 11</span></div><span class="b-badge b-badge-ok" style="margin-left:6px">Paid</span><span class="amt">€537.56</span></div>
1552
+ <div class="m-row"><div><b>#1040</b><span class="s">Ana Costa · Jul 10</span></div><span class="b-badge b-badge-info" style="margin-left:6px">Shipped</span><span class="amt">€1,041.00</span></div>
1553
+ <div class="m-row"><div><b>#1039</b><span class="s">Leo Martens · Jul 10</span></div><span class="b-badge b-badge-ok" style="margin-left:6px">Paid</span><span class="amt">€49.90</span></div>
1554
+ <div class="m-row"><div><b>#1038</b><span class="s">Emma Klein · Jul 9</span></div><span class="b-badge b-badge-mute" style="margin-left:6px">Refunded</span><span class="amt">€12.50</span></div>
1555
+ </div>
1556
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">
1557
+ <span style="font-size:11.5px;color:var(--muted)" class="num">1–25 of 312</span>
1558
+ <span style="display:flex;gap:8px"><button class="b-btn b-btn-secondary" style="height:40px;opacity:.5" disabled>Previous</button><button class="b-btn b-btn-secondary" style="height:40px">Next</button></span>
1559
+ </div>
1560
+ </div>
1561
+ <div class="ph-home"></div>
1562
+ </div>
1563
+ </div>
1564
+ <p style="text-align:center;font-size:11.5px;color:var(--pg-muted);margin-top:8px;max-width:350px"><b style="color:var(--pg-ink)">Index pages.</b> Full-width 44px search, chips scroll on one line, low-priority columns hidden, bulk selection rests until desktop (Shopify's own call).</p>
1565
+ </div>
1566
+
1567
+ <!-- PHONE C · form + save bar + sheet + toast -->
1568
+ <div>
1569
+ <div class="ph" id="ph-c">
1570
+ <div class="ph-status"><span>9:41</span><i>●●● ▲ ▮</i></div>
1571
+ <div class="ph-body">
1572
+ <div class="m-top">
1573
+ <button class="m-burger" aria-label="Open menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></button>
1574
+ <span class="m-tile">E</span>
1575
+ <span class="m-name">USB-C Hub 7-in-1</span>
1576
+ <span style="flex:1"></span>
1577
+ <span class="m-av">GB</span>
1578
+ </div>
1579
+ <div class="m-scroll" style="padding-bottom:120px">
1580
+ <div class="b-card b-card-pad" style="padding:14px;margin-bottom:10px">
1581
+ <div class="b-label" style="margin-bottom:6px">Name</div>
1582
+ <label class="b-input" style="height:44px;margin-bottom:12px"><input value="USB-C Hub 7-in-1"></label>
1583
+ <div class="b-label" style="margin-bottom:6px">Price</div>
1584
+ <div class="os-addon" style="margin-bottom:12px"><span class="pre" style="display:flex;align-items:center">€</span><input value="49.90" style="height:44px"></div>
1585
+ <div style="display:flex;align-items:center;gap:10px;font-size:13px"><button class="b-switch" role="switch" aria-checked="true" aria-label="Charge tax"></button>Charge tax on this product</div>
1586
+ </div>
1587
+ <button class="b-btn b-btn-secondary" id="ph-c-open" style="height:40px;width:100%;justify-content:center">Open the delete confirm</button>
1588
+ </div>
1589
+ <div class="m-toast"><svg><use href="#i-check"/></svg>Product saved</div>
1590
+ <div class="m-savebar">
1591
+ <span class="dot"></span><span class="lbl">Unsaved changes</span><span class="sp"></span>
1592
+ <button class="b-btn b-btn-ghost">Discard</button><button class="b-btn b-btn-primary">Save</button>
1593
+ </div>
1594
+ <div class="m-scrim" data-close="ph-c"></div>
1595
+ <div class="m-sheet">
1596
+ <div class="grab"></div>
1597
+ <h4>Delete this product?</h4>
1598
+ <p>It will be removed from your storefront immediately. Products with order history are archived instead.</p>
1599
+ <div class="acts"><button class="b-btn b-btn-ghost" data-close="ph-c">Cancel</button><button class="b-btn b-btn-danger-solid" data-close="ph-c">Delete product</button></div>
1600
+ </div>
1601
+ <div class="ph-home"></div>
1602
+ </div>
1603
+ </div>
1604
+ <p style="text-align:center;font-size:11.5px;color:var(--pg-muted);margin-top:8px;max-width:350px"><b style="color:var(--pg-ink)">Forms + feedback.</b> 44px fields, the save bar docks full-width above the home indicator, modals become bottom sheets, toasts span the width.</p>
1605
+ </div>
1606
+ </div>
1607
+
1608
+ <!-- Spec cells -->
1609
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
1610
+ <div class="os-cell">
1611
+ <div class="os-lab">The three breakpoints — nothing else</div>
1612
+ <div class="os-vs" style="text-decoration:none">
1613
+ <div style="text-decoration:none;color:var(--ink);font-weight:600;background:#fff">&lt; 480px · phones</div><div>optional table columns + bulk checkboxes hide · toasts span the width · top-bar labels go icon-only</div>
1614
+ <div style="text-decoration:none;color:var(--ink);font-weight:600;background:#fff">&lt; 640px · phones + small</div><div>AdminModal becomes a bottom sheet (slide-up, top radius, safe-area footer)</div>
1615
+ <div style="text-decoration:none;color:var(--ink);font-weight:600;background:#fff">&lt; 768px · the shell pivot</div><div>sidebar → drawer + burger · save bar full-width (rule already exists) · detail grids already stack at 1024</div>
1616
+ </div>
1617
+ <div class="os-note"><b>Why 768 and not the platform's 1024:</b> the admin lives inside the platform's preview iframe, often 800–1000px wide on laptops — pivoting at 1024 would put the drawer inside the desktop editing experience. 768 keeps the approved desktop chrome everywhere it fits and matches the save bar's existing mobile rule.</div>
1618
+ </div>
1619
+ <div class="os-cell">
1620
+ <div class="os-lab">Touch + platform ergonomics</div>
1621
+ <div class="os-vs">
1622
+ <div>36px buttons · 32px chips/nav · 28px top-bar pills</div><div>@media (pointer: coarse): 44px controls, 44px nav rows, 36px chips — desktop density untouched</div>
1623
+ <div>Toast max-width 416px overflows a 375px phone</div><div>toasts clamp to the viewport with 12px gutters</div>
1624
+ <div>No safe-area handling anywhere</div><div>env(safe-area-inset-bottom) on the save bar, sheets and toaster (the cookie banner's proven recipe)</div>
1625
+ <div>CustomerTable + UsersTable can blow the page sideways</div><div>every table gets the overflow-x wrapper guarantee</div>
1626
+ </div>
1627
+ <div class="os-note"><b>Deliberately NOT doing:</b> no bottom tab bar (native-app pattern — Shopify's mobile web admin uses a drawer), no card-transformation of tables in v1 (column priority + the scroll guarantee is Polaris' own condensed logic at a fraction of the risk), no new dependencies (the drawer animates with pure CSS — the SDK has no motion library, and keeps it that way).</div>
1628
+ </div>
1629
+ </div>
1630
+
1631
+ </div></div>
1632
+ </div>
1633
+ <div class="art-caption">
1634
+ <span><b>try it:</b> tap the burger on phone A · open the delete confirm on phone C — the modal arrives as a bottom sheet</span>
1635
+ <span><b>where the code lands:</b> ~90% additive media queries in admin-theme.css + ~60 lines of drawer wiring in AdminLayout/TopBar/Nav — the shell components barely change</span>
1636
+ <span>the scroll lock is the AdminModal counter, shared — a drawer over a locked body, like every overlay</span>
1637
+ <span>same LEDGER: one accent, washed bands, quiet icons — nothing gets a new visual language on mobile</span>
1638
+ </div>
1639
+ </div>
1640
+ </div>
1641
+ </section>
1642
+
1643
+ <!-- ═══════════════════════ LIGHTER CODE ═══════════════════════ -->
1644
+ <section id="lighter">
1645
+ <div class="wrap">
1646
+ <div class="sec-eyebrow">Section 5</div>
1647
+ <h2 class="sec-title">Why the code gets lighter</h2>
1648
+ <p class="sec-note">Today each page hand-assembles its header, filters, buttons and confirmations, which is where the inconsistency comes from. Under LEDGER, pages stop owning visual decisions: they instantiate primitives. The CSS shrinks because variants disappear, and new surfaces are consistent by construction because there is nothing else to reach for.</p>
1649
+ <div class="cols cols-2">
1650
+ <div class="panel">
1651
+ <h3>Before · every page invents</h3>
1652
+ <pre class="code"><span class="c">// products/page.tsx today</span>
1653
+ &lt;div className="flex items-center justify-between"&gt;
1654
+ &lt;div&gt;
1655
+ &lt;h1 className="admin-page-title"&gt;Products&lt;/h1&gt;
1656
+ &lt;p className="admin-page-subtitle"&gt;Manage your…&lt;/p&gt;
1657
+ &lt;/div&gt;
1658
+ &lt;button className="inline-flex items-center gap-2
1659
+ rounded-md bg-primary px-4 py-2 text-sm…"&gt;
1660
+ Add Product
1661
+ &lt;/button&gt;
1662
+ &lt;/div&gt;
1663
+ <span class="c">// + its own confirm markup, its own filter stack,
1664
+ // + per-row delete buttons, per-page spacing…</span></pre>
1665
+ </div>
1666
+ <div class="panel">
1667
+ <h3>After · every page composes</h3>
1668
+ <pre class="code"><span class="c">// any index page</span>
1669
+ &lt;<span class="k">AdminPageHeader</span> title="Products" subtitle="84,907 active"
1670
+ primary={{ label: 'Add product', icon: Plus }} /&gt;
1671
+ &lt;<span class="k">AdminFilterRow</span> search chips={views} sort={sorts} /&gt;
1672
+ &lt;<span class="k">AdminTable</span> … /&gt;
1673
+
1674
+ <span class="c">// any form page</span>
1675
+ &lt;<span class="k">AdminPageHeader</span> back title={product.name} badge="Active" /&gt;
1676
+ &lt;<span class="k">AdminSaveBar</span> when={isDirty} onSave={save} /&gt;
1677
+
1678
+ <span class="c">// any destructive action</span>
1679
+ confirm({ title: 'Delete discount?', danger: true })</pre>
1680
+ </div>
1681
+ </div>
1682
+ <div class="panel">
1683
+ <h3>The contract in one table</h3>
1684
+ <div style="overflow-x:auto">
1685
+ <table style="width:100%;border-collapse:collapse;font-size:12.5px">
1686
+ <thead><tr>
1687
+ <th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--pg-border);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--pg-subtle)">Job</th>
1688
+ <th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--pg-border);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--pg-subtle)">The one way to do it</th>
1689
+ <th style="text-align:left;padding:8px 10px;border-bottom:1px solid var(--pg-border);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--pg-subtle)">Replaces</th>
1690
+ </tr></thead>
1691
+ <tbody>
1692
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Open a page</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminPageHeader (back · eyebrow · title · badge · subtitle · actions)</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">20 hand-rolled headers, floating back arrows, double card titles</td></tr>
1693
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Filter a list</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminFilterRow: search + view chips + sort, one line, swaps to bulk bar</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">Stacked full-width selects, native dropdowns, three-row filter blocks</td></tr>
1694
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Act on a row</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">Row = link, one ⋯ menu for everything else</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">Per-row status dropdowns, pencil + trash pairs, green text-link triplets</td></tr>
1695
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Save a form</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminSaveBar on dirty state</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">Floating Save pills, per-card Save buttons</td></tr>
1696
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Confirm anything</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">One modal: object + consequence + Cancel / verb button</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">Mixed inline confirms, divergent dialogs, browser confirm()</td></tr>
1697
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Show status</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">One badge recipe, five tones + neutral</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">Solid fills, accent-colored states, black donut segments</td></tr>
1698
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Overlay anything</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminModal: portal, blur, Esc, guarded click-outside, scroll lock, three widths</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">8 hand-rolled dialogs, 4 backdrop styles, Esc on 1 of 8</td></tr>
1699
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Show an empty list</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminEmptyState: first-run invites creation, no-match offers clearing filters</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">33 sites in 11 shapes, one CTA in the whole admin</td></tr>
1700
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Search + paginate</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminSearchInput (300ms debounce) + AdminPagination</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">3 search markups, fetch-per-keystroke, 4 pagination re-rolls</td></tr>
1701
+ <tr><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);font-weight:600">Toggle a state</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border)">AdminSwitch everywhere; checkboxes mean selection only</td><td style="padding:8px 10px;border-bottom:1px solid var(--pg-border);color:var(--pg-muted)">3 switch builds + checkbox-as-toggle in every manager</td></tr>
1702
+ <tr><td style="padding:8px 10px;font-weight:600">Wait for data</td><td style="padding:8px 10px">Skeletons on first visit, content stays on refetch, one spinner</td><td style="padding:8px 10px;color:var(--pg-muted)">3 spinner builds, tables blanking on background reloads</td></tr>
1703
+ </tbody>
1704
+ </table>
1705
+ </div>
1706
+ </div>
1707
+ </div>
1708
+ </section>
1709
+
1710
+ <footer>
1711
+ <div class="wrap">
1712
+ LEDGER · Rovela Admin design language proposal · drafted July 10, 2026. Revised July 11 with tab 07 (One way each), the consolidation kit from the full-admin consistency audit. Tab 08 (Mobile) added the same day: the LEDGER at thumb size — drawer shell, condensed lists, bottom sheets, 44px touch targets. The artboards use EasyElec production data and the exact tokens already landed in admin-theme.css. The spec page itself follows the laws it proposes.
1713
+ </div>
1714
+ </footer>
1715
+
1716
+ <script>
1717
+ (function(){
1718
+ // pattern tab switcher
1719
+ document.querySelectorAll('.ptab').forEach(function(tab){
1720
+ tab.addEventListener('click', function(){
1721
+ document.querySelectorAll('.ptab').forEach(function(t){ t.classList.remove('on') });
1722
+ document.querySelectorAll('.artwrap').forEach(function(a){ a.classList.remove('on') });
1723
+ tab.classList.add('on');
1724
+ document.getElementById(tab.dataset.art).classList.add('on');
1725
+ });
1726
+ });
1727
+
1728
+ // row menus (⋯)
1729
+ function closeMenus(){ document.querySelectorAll('.b-menu.open').forEach(function(m){ if(!m.dataset.pinned) m.classList.remove('open') }) }
1730
+ document.querySelectorAll('.row-menu-btn').forEach(function(btn){
1731
+ btn.addEventListener('click', function(e){
1732
+ e.stopPropagation();
1733
+ var menu = btn.parentElement.querySelector('.b-menu');
1734
+ var wasOpen = menu.classList.contains('open');
1735
+ closeMenus();
1736
+ if(!wasOpen) menu.classList.add('open');
1737
+ });
1738
+ });
1739
+ document.addEventListener('click', closeMenus);
1740
+
1741
+ // orders bulk bar
1742
+ var list = document.getElementById('orders-list');
1743
+ if(list){
1744
+ var rows = list.querySelectorAll('.row-check');
1745
+ var all = document.getElementById('check-all');
1746
+ function sync(){
1747
+ var n = list.querySelectorAll('.row-check:checked').length;
1748
+ list.classList.toggle('bulk-on', n > 0);
1749
+ var c = document.getElementById('bulk-count');
1750
+ if(c) c.textContent = n + ' selected';
1751
+ if(all){ all.checked = n === rows.length && n > 0; }
1752
+ }
1753
+ rows.forEach(function(ch){ ch.addEventListener('change', sync); ch.addEventListener('click', function(e){ e.stopPropagation() }) });
1754
+ if(all){ all.addEventListener('change', function(){ rows.forEach(function(ch){ ch.checked = all.checked }); sync() }) }
1755
+ var clear = document.getElementById('bulk-clear');
1756
+ if(clear){ clear.addEventListener('click', function(){ rows.forEach(function(ch){ ch.checked=false }); if(all) all.checked=false; sync() }) }
1757
+ }
1758
+
1759
+ // save bar demo — dirty tracking with changed-field summary, then the
1760
+ // saving → saved morph (the bar IS the feedback; no separate toast)
1761
+ var savebar = document.getElementById('savebar');
1762
+ if(savebar){
1763
+ var sbLabel = document.getElementById('sb-label');
1764
+ var sbMeta = document.getElementById('sb-meta');
1765
+ var saveBtn = document.getElementById('save-btn');
1766
+ var discardBtn = document.getElementById('discard-btn');
1767
+ var FIELD_LABELS = { 'f-name':'Name', 'f-desc':'Description', 'f-price':'Price', 'f-cmp':'Compare-at', 'f-sku':'SKU' };
1768
+ var touched = {};
1769
+ function resetBar(){
1770
+ savebar.classList.remove('show','saving','saved');
1771
+ sbLabel.textContent = 'Unsaved changes';
1772
+ sbMeta.textContent = '';
1773
+ saveBtn.textContent = 'Save';
1774
+ saveBtn.disabled = false; discardBtn.disabled = false;
1775
+ touched = {};
1776
+ }
1777
+ document.querySelectorAll('.dirty-watch').forEach(function(inp){
1778
+ inp.addEventListener('input', function(){
1779
+ touched[inp.id || 'vendor'] = FIELD_LABELS[inp.id] || 'Vendor';
1780
+ var names = Object.keys(touched).map(function(k){ return touched[k] });
1781
+ sbMeta.textContent = names.slice(0,3).join(' · ') + (names.length > 3 ? ' · …' : '');
1782
+ savebar.classList.remove('saved','saving');
1783
+ sbLabel.textContent = 'Unsaved changes';
1784
+ savebar.classList.add('show');
1785
+ });
1786
+ });
1787
+ discardBtn.addEventListener('click', resetBar);
1788
+ saveBtn.addEventListener('click', function(){
1789
+ savebar.classList.add('saving');
1790
+ sbLabel.textContent = 'Saving…'; saveBtn.textContent = 'Saving…';
1791
+ saveBtn.disabled = true; discardBtn.disabled = true;
1792
+ setTimeout(function(){
1793
+ savebar.classList.remove('saving');
1794
+ savebar.classList.add('saved');
1795
+ sbLabel.textContent = 'Saved';
1796
+ sbMeta.textContent = 'All changes are live in your store';
1797
+ setTimeout(resetBar, 1400);
1798
+ }, 900);
1799
+ });
1800
+ }
1801
+
1802
+ // discounts delete flow
1803
+ var overlay = document.getElementById('mgr-overlay');
1804
+ if(overlay){
1805
+ var codeEl = document.getElementById('del-code');
1806
+ var mgrToast = document.getElementById('mgr-toast');
1807
+ document.querySelectorAll('.del-discount').forEach(function(btn){
1808
+ btn.addEventListener('click', function(e){
1809
+ e.stopPropagation();
1810
+ closeMenus();
1811
+ codeEl.textContent = btn.dataset.code;
1812
+ overlay.classList.add('open');
1813
+ });
1814
+ });
1815
+ document.getElementById('del-cancel').addEventListener('click', function(){ overlay.classList.remove('open') });
1816
+ overlay.addEventListener('click', function(e){ if(e.target === overlay) overlay.classList.remove('open') });
1817
+ document.getElementById('del-confirm').addEventListener('click', function(){
1818
+ overlay.classList.remove('open');
1819
+ mgrToast.classList.add('show');
1820
+ setTimeout(function(){ mgrToast.classList.remove('show') }, 2200);
1821
+ });
1822
+ }
1823
+
1824
+ // Toast playground — the ONE toaster: stack of 3, success auto-dismisses,
1825
+ // errors sticky with a dismiss
1826
+ var toaster = document.getElementById('toaster');
1827
+ if(toaster){
1828
+ var dismissToast = function(el){
1829
+ el.classList.add('leaving');
1830
+ setTimeout(function(){ el.remove() }, 200);
1831
+ };
1832
+ var spawnToast = function(kind){
1833
+ while(toaster.children.length >= 3){ toaster.removeChild(toaster.firstChild) }
1834
+ var t = document.createElement('div');
1835
+ t.className = 'b-toast';
1836
+ if(kind === 'err'){
1837
+ t.className = 'b-toast err';
1838
+ t.innerHTML = '<svg style="width:14px;height:14px"><use href="#i-x"/></svg>' +
1839
+ "Couldn't send the invite. Check the email address and try again." +
1840
+ '<span class="b-toast-x"><svg style="width:12px;height:12px"><use href="#i-x"/></svg></span>';
1841
+ t.querySelector('.b-toast-x').addEventListener('click', function(){ dismissToast(t) });
1842
+ } else if(kind === 'action'){
1843
+ t.innerHTML = '<svg style="width:14px;height:14px"><use href="#i-check"/></svg>Test email sent to you<span class="b-toast-action">View email</span>';
1844
+ setTimeout(function(){ if(t.parentNode) dismissToast(t) }, 3000);
1845
+ } else {
1846
+ t.innerHTML = '<svg style="width:14px;height:14px"><use href="#i-check"/></svg>Discount deleted';
1847
+ setTimeout(function(){ if(t.parentNode) dismissToast(t) }, 3000);
1848
+ }
1849
+ toaster.appendChild(t);
1850
+ };
1851
+ var fo = document.getElementById('fire-ok');
1852
+ var fa = document.getElementById('fire-action');
1853
+ var fe = document.getElementById('fire-err');
1854
+ if(fo) fo.addEventListener('click', function(){ spawnToast('ok') });
1855
+ if(fa) fa.addEventListener('click', function(){ spawnToast('action') });
1856
+ if(fe) fe.addEventListener('click', function(){ spawnToast('err') });
1857
+ }
1858
+
1859
+ // AUTH: eye toggle + submit -> loading -> inline error state demo
1860
+ var authEye = document.getElementById('auth-eye');
1861
+ var authPw = document.getElementById('auth-pw');
1862
+ if(authEye && authPw){
1863
+ authEye.addEventListener('click', function(){
1864
+ authPw.type = authPw.type === 'password' ? 'text' : 'password';
1865
+ });
1866
+ }
1867
+ var authSubmit = document.getElementById('auth-submit');
1868
+ var authAlert = document.getElementById('auth-alert');
1869
+ if(authSubmit && authAlert){
1870
+ authSubmit.addEventListener('click', function(){
1871
+ authAlert.classList.remove('show');
1872
+ authSubmit.disabled = true;
1873
+ authSubmit.textContent = 'Signing in…';
1874
+ setTimeout(function(){
1875
+ authSubmit.disabled = false;
1876
+ authSubmit.textContent = 'Sign in';
1877
+ authAlert.classList.add('show');
1878
+ }, 900);
1879
+ });
1880
+ }
1881
+
1882
+ // 07 · live AdminModal demo: Esc + backdrop + buttons all close
1883
+ var osOverlay = document.getElementById('os-overlay');
1884
+ if(osOverlay){
1885
+ var closeOs = function(){ osOverlay.classList.remove('open') };
1886
+ document.getElementById('os-modal-open').addEventListener('click', function(){ osOverlay.classList.add('open') });
1887
+ document.getElementById('os-modal-cancel').addEventListener('click', closeOs);
1888
+ document.getElementById('os-modal-x').addEventListener('click', closeOs);
1889
+ document.getElementById('os-modal-go').addEventListener('click', closeOs);
1890
+ osOverlay.addEventListener('click', function(e){ if(e.target === osOverlay) closeOs() });
1891
+ document.addEventListener('keydown', function(e){ if(e.key === 'Escape') closeOs() });
1892
+ }
1893
+
1894
+ // 07 · empty-state variant toggle
1895
+ document.querySelectorAll('#os-empty-seg button').forEach(function(b){
1896
+ b.addEventListener('click', function(){
1897
+ document.querySelectorAll('#os-empty-seg button').forEach(function(x){ x.classList.remove('on') });
1898
+ b.classList.add('on');
1899
+ document.getElementById('em-first').style.display = b.dataset.em === 'em-first' ? '' : 'none';
1900
+ document.getElementById('em-nomatch').style.display = b.dataset.em === 'em-nomatch' ? '' : 'none';
1901
+ });
1902
+ });
1903
+
1904
+ // 07 · live list search filter
1905
+ var osSearch = document.getElementById('os-search');
1906
+ if(osSearch){
1907
+ osSearch.addEventListener('input', function(){
1908
+ var q = osSearch.value.toLowerCase();
1909
+ document.querySelectorAll('#os-list tbody tr').forEach(function(tr){
1910
+ tr.style.display = tr.textContent.toLowerCase().indexOf(q) > -1 ? '' : 'none';
1911
+ });
1912
+ });
1913
+ }
1914
+
1915
+ // 07 · chips are single-select
1916
+ document.querySelectorAll('#os-chips .b-chip').forEach(function(ch){
1917
+ ch.addEventListener('click', function(){
1918
+ document.querySelectorAll('#os-chips .b-chip').forEach(function(x){ x.classList.remove('on') });
1919
+ ch.classList.add('on');
1920
+ });
1921
+ });
1922
+
1923
+ // 07 · switches
1924
+ document.querySelectorAll('.os-sw').forEach(function(sw){
1925
+ sw.addEventListener('click', function(){
1926
+ sw.setAttribute('aria-checked', sw.getAttribute('aria-checked') === 'true' ? 'false' : 'true');
1927
+ });
1928
+ });
1929
+
1930
+ // 07 · button in-flight demo
1931
+ var osSave = document.getElementById('os-save');
1932
+ if(osSave){
1933
+ osSave.addEventListener('click', function(){
1934
+ if(osSave.disabled) return;
1935
+ osSave.disabled = true;
1936
+ osSave.innerHTML = '<span class="b-spin sm inv"></span>Saving…';
1937
+ setTimeout(function(){ osSave.disabled = false; osSave.textContent = 'Save'; }, 1100);
1938
+ });
1939
+ }
1940
+
1941
+ // 08 · phone demos: drawer + bottom sheet
1942
+ var phA = document.getElementById('ph-a');
1943
+ if(phA){
1944
+ document.getElementById('ph-a-burger').addEventListener('click', function(){ phA.classList.add('nav-open') });
1945
+ document.querySelectorAll('[data-close="ph-a"]').forEach(function(el){ el.addEventListener('click', function(){ phA.classList.remove('nav-open') }) });
1946
+ phA.querySelectorAll('.m-drawer nav a').forEach(function(a){ a.addEventListener('click', function(){ phA.classList.remove('nav-open') }) });
1947
+ }
1948
+ var phC = document.getElementById('ph-c');
1949
+ if(phC){
1950
+ document.getElementById('ph-c-open').addEventListener('click', function(){ phC.classList.add('sheet-open') });
1951
+ document.querySelectorAll('[data-close="ph-c"]').forEach(function(el){ el.addEventListener('click', function(){ phC.classList.remove('sheet-open') }) });
1952
+ }
1953
+
1954
+ // keep artboard rows from navigating
1955
+ document.querySelectorAll('.b-table tbody tr').forEach(function(tr){
1956
+ tr.addEventListener('click', function(){ /* row is the link in product; inert in mock */ });
1957
+ });
1958
+ })();
1959
+ </script>
1960
+
1961
+ </body></html>