actual-css 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1087 -643
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +62 -30
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +53 -7
  18. package/docs/design-notes/popover-manual-poc.md +105 -25
  19. package/docs/design-notes/surface-reparenting.md +56 -47
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +7 -4
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +85 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +21 -6
  30. package/docs/pages/components/drawer.md +13 -0
  31. package/docs/pages/components/fab.md +4 -3
  32. package/docs/pages/components/list.md +42 -4
  33. package/docs/pages/components/navbar.md +17 -1
  34. package/docs/pages/components/pagination.md +2 -2
  35. package/docs/pages/components/skeleton.md +3 -1
  36. package/docs/pages/components/steps.md +19 -0
  37. package/docs/pages/components/table.md +3 -2
  38. package/docs/pages/enhancements/flyout.md +82 -38
  39. package/docs/pages/enhancements/overview.md +25 -0
  40. package/docs/pages/enhancements/status-bar.md +1 -1
  41. package/docs/pages/enhancements/tabs.md +45 -37
  42. package/docs/pages/enhancements/tooltip.md +36 -10
  43. package/docs/pages/examples/overview.md +38 -5
  44. package/docs/pages/forms/choice-cards.md +11 -6
  45. package/docs/pages/forms/combobox.md +1 -1
  46. package/docs/pages/forms/floating-field.md +4 -3
  47. package/docs/pages/forms/joined-controls.md +22 -0
  48. package/docs/pages/forms/otp.md +2 -1
  49. package/docs/pages/forms/overview.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +16 -18
  51. package/docs/pages/forms/sticky-actions.md +2 -2
  52. package/docs/pages/forms/switch.md +1 -1
  53. package/docs/pages/foundations/philosophy.md +57 -26
  54. package/docs/pages/foundations/theming.md +2 -2
  55. package/docs/pages/foundations/tokens.md +118 -21
  56. package/docs/pages/foundations/typography.md +35 -14
  57. package/docs/pages/guides/introduction.md +11 -9
  58. package/docs/pages/guides/progressive-enhancement.md +115 -30
  59. package/docs/pages/guides/tailwind.md +3 -2
  60. package/docs/pages/layout/media.md +1 -1
  61. package/docs/pages/layout/overview.md +11 -1
  62. package/docs/pages/layout/scroll-snap.md +3 -3
  63. package/docs/pages/layout/stack.md +17 -9
  64. package/docs/pages/patterns/icon-input.md +4 -2
  65. package/docs/pages/patterns/icon-slot.md +2 -0
  66. package/docs/pages/patterns/intent-nesting.md +1 -1
  67. package/docs/pages/utilities/overview.md +23 -2
  68. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  69. package/docs/pages/utilities/spacing.md +10 -8
  70. package/llms.txt +2 -1
  71. package/package.json +159 -149
  72. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  73. package/src/css/components/accordion.css +42 -3
  74. package/src/css/components/alert.css +25 -13
  75. package/src/css/components/app-nav.css +18 -8
  76. package/src/css/components/avatar.css +13 -1
  77. package/src/css/components/badge.css +78 -12
  78. package/src/css/components/breadcrumb.css +2 -3
  79. package/src/css/components/button.css +9 -16
  80. package/src/css/components/card.css +9 -2
  81. package/src/css/components/chat.css +1 -0
  82. package/src/css/components/data-list.css +1 -1
  83. package/src/css/components/drawer.css +101 -32
  84. package/src/css/components/fab.css +0 -6
  85. package/src/css/components/flyout.css +35 -98
  86. package/src/css/components/index.css +0 -1
  87. package/src/css/components/indicator.css +21 -0
  88. package/src/css/components/key.css +10 -0
  89. package/src/css/components/list.css +40 -4
  90. package/src/css/components/menu.css +2 -1
  91. package/src/css/components/modal.css +48 -33
  92. package/src/css/components/navbar.css +37 -4
  93. package/src/css/components/pagination.css +1 -0
  94. package/src/css/components/skeleton.css +15 -4
  95. package/src/css/components/spinner.css +6 -6
  96. package/src/css/components/status-bar.css +0 -5
  97. package/src/css/components/steps.css +45 -59
  98. package/src/css/components/tab.css +29 -9
  99. package/src/css/components/table.css +13 -5
  100. package/src/css/components/tooltip.css +36 -40
  101. package/src/css/core/base.css +23 -1
  102. package/src/css/core/intents.css +17 -6
  103. package/src/css/core/theme.css +27 -0
  104. package/src/css/core/tokens.css +35 -15
  105. package/src/css/core/variants.css +64 -15
  106. package/src/css/forms/choice-card.css +4 -4
  107. package/src/css/forms/choice.css +76 -29
  108. package/src/css/forms/control.css +2 -1
  109. package/src/css/forms/floating-field.css +3 -3
  110. package/src/css/forms/form.css +4 -3
  111. package/src/css/forms/input-icon.css +20 -14
  112. package/src/css/forms/native.css +6 -1
  113. package/src/css/forms/otp.css +9 -1
  114. package/src/css/forms/range.css +2 -0
  115. package/src/css/forms/select.css +29 -9
  116. package/src/css/forms/switch.css +28 -8
  117. package/src/css/layout/app-layout.css +1 -1
  118. package/src/css/layout/grid.css +50 -76
  119. package/src/css/layout/scroll-snap.css +11 -7
  120. package/src/css/layout/scroller.css +3 -4
  121. package/src/css/layout/stack.css +2 -2
  122. package/src/css/typography/fluid.css +4 -3
  123. package/src/css/typography/prose.css +6 -5
  124. package/src/css/utilities/base.css +2 -1
  125. package/src/css/utilities/extra.css +20 -17
  126. package/src/js/context-menu.js +27 -26
  127. package/src/js/dialog.js +2 -1
  128. package/src/js/dismiss.js +5 -0
  129. package/src/js/enhance.js +34 -2
  130. package/src/js/enhancement-loader.js +36 -43
  131. package/src/js/events.js +0 -3
  132. package/src/js/filter.js +0 -1
  133. package/src/js/floating-space.js +42 -0
  134. package/src/js/flyout.js +9 -44
  135. package/src/js/focus-group.js +7 -0
  136. package/src/js/focus.js +6 -0
  137. package/src/js/menu.js +10 -1
  138. package/src/js/selectors.js +5 -9
  139. package/src/js/status.js +6 -5
  140. package/src/js/surface.js +97 -139
  141. package/src/js/tab.js +6 -1
  142. package/src/js/tooltip.js +150 -51
  143. package/src/js/validation.js +1 -1
  144. package/src/css/components/surface.css +0 -49
package/dist/actual.js CHANGED
@@ -1 +1 @@
1
- var M=new WeakMap,b=new WeakMap;function N(){return{refresh(){},disconnect(){}}}function T(e){return e?.nodeType===1||e?.nodeType===9||e?.nodeType===11}function _(e){let t=new Set,r="";function n(o){try{if(e.querySelector?.(o),e.nodeType===1)e.matches(o);return!0}catch(c){return console.error(`Invalid enhancer selector "${o}"`,c),!1}}function s(){r=[...t].flatMap((o)=>o.selectors).join(",")}function a(o,c){if(c?.nodeType!==1)return;let i=o.instances.get(c);for(let f of o.selectors){if(!c.matches(f))continue;if(i?.has(f))continue;let d;try{d=o.enhancers[f](c)}catch(U){console.error(`Enhancer failed for selector "${f}"`,U);continue}if(!i)i=new Map,o.instances.set(c,i);i.set(f,typeof d==="function"?d:null)}}function u(o,c){let i=o.instances.get(c);if(!i)return;for(let f of i.values())try{f?.()}catch(d){console.error("Enhancer cleanup failed",d)}o.instances.delete(c)}function l(o,c){if(!T(c))return;if(c.nodeType===1)a(o,c);if(o.selectorString)c.querySelectorAll?.(o.selectorString).forEach((i)=>{a(o,i)})}function p(o){if(!T(o)||!r)return;if(o.nodeType===1)for(let c of t)a(c,o);o.querySelectorAll?.(r).forEach((c)=>{for(let i of t)a(i,c)})}function g(){for(let o of t)for(let c of Array.from(o.instances.keys()))if(!c.isConnected||!e.contains(c))u(o,c)}let h=new MutationObserver((o)=>{let c=!1;for(let i of o){for(let f of i.addedNodes)p(f);if(i.removedNodes.length>0)c=!0}if(c)g()});return h.observe(e,{childList:!0,subtree:!0}),{add(o){let c=Object.keys(o).filter(n);if(!c.length)return null;let f={disconnected:!1,enhancers:Object.fromEntries(c.map((d)=>[d,o[d]])),instances:new Map,selectors:c,selectorString:c.join(",")};return t.add(f),s(),l(f,e),f},refresh(o,c){if(o.disconnected)return;l(o,c)},remove(o){if(o.disconnected)return;o.disconnected=!0;for(let c of Array.from(o.instances.keys()))u(o,c);if(t.delete(o),s(),t.size===0)h.disconnect(),M.delete(e)}}}var A=/^[a-z][a-z0-9-]*$/;function O(e){if(!A.test(e))throw TypeError(`Invalid enhancement name: ${e}`);return`[data-enhance~="${e}"]`}function S(e,t,r){if(typeof document>"u")return N();if(O(e),typeof t!=="function")throw TypeError("registerEnhancement() requires a function init.");r??=document.documentElement;let n=b.get(r);if(!n)n=new Map,b.set(r,n);if(n.has(e))throw Error(`Enhancement "${e}" is already registered on this root.`);let s=E({[O(e)]:t},r);return n.set(e,s),{refresh:(a)=>s.refresh(a),disconnect:()=>{if(s.disconnect(),n.get(e)===s)n.delete(e);if(n.size===0)b.delete(r)}}}function R(e){let t=M.get(e);if(!t)t=_(e),M.set(e,t);return t}function E(e,t){if(typeof document>"u")return N();t??=document.documentElement;let r=R(t),n=r.add(e);if(!n)return N();return{refresh:(s)=>r.refresh(n,s),disconnect:()=>{r.remove(n)}}}function j(e){return e.replace(/([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,(t,r,n)=>{if(r)return`"${r}":`;return`"${n.replace(/\\'/g,"'").replace(/"/g,"\\\"")}"`})}function v(e){if(e==null||typeof e!=="string")return{};let t=e.trim();if(t==="")return{};let r=t,n;try{n=JSON.parse(r)}catch{let s=j(r);r=s.startsWith("{")?s:`{${s}}`,n=JSON.parse(r)}if(n===null||typeof n!=="object"||Array.isArray(n))throw TypeError("Enhancement config must be an object");return n}var L='script[type="application/json"][data-enhance-modules]',m=new Map,y=new Map,w=new Map,k=(e)=>import(e);function D(e){let t=new URL(e,document.baseURI).href;if(!y.has(t))y.set(t,new Promise((r,n)=>{let s=document.createElement("script");s.src=t,s.onload=r,s.onerror=()=>{y.delete(t),s.remove(),n(Error(`Unable to load script: ${t}`))},document.head.append(s)}));return y.get(t)}function C(e){let t=new URL(e,document.baseURI).href;if(!w.has(t))w.set(t,new Promise((r,n)=>{let s=document.createElement("link");s.rel="stylesheet",s.href=t,s.onload=r,s.onerror=()=>{w.delete(t),s.remove(),n(Error(`Unable to load style: ${t}`))},document.head.append(s)}));return w.get(t)}function I(e,t){return(r)=>{let n=new AbortController,s=r.getAttribute("data-enhance-config"),a=v(s??""),u=(p,g,h={})=>r.dispatchEvent(new CustomEvent(`actual:${e}:${p}`,{bubbles:!0,detail:g,...h})),l;try{l=t(r,{config:a,signal:n.signal,emit:u})}catch(p){throw n.abort(),p}if(l?.then)throw n.abort(),TypeError(`Enhancement "${e}" returned a Promise. connect() must be synchronous; use prepare() for asynchronous setup.`);if(l!==void 0&&typeof l!=="function")throw n.abort(),TypeError(`Enhancement "${e}" must return a cleanup function or undefined.`);return()=>{n.abort(),l?.()}}}function J(e,t){if(typeof t!=="string"||t.trim()==="")throw TypeError(`Enhancement "${e}" must declare a non-empty module URL.`)}function F(e,t){J(e,t);let r=new URL(t,document.baseURI).href,n=m.get(e);if(n){if(n.url!==r)throw Error(`Enhancement "${e}" is already declared from ${n.url} and cannot be replaced by ${r}.`);return n.promise.then(()=>({name:e,status:"skipped"}))}let s=k(r).then(async(a)=>{if(typeof a.default!=="function")throw TypeError(`Enhancement "${e}" must export a default function.`);if(a.prepare!==void 0){if(typeof a.prepare!=="function")throw TypeError(`Enhancement "${e}" exports an invalid prepare().`);await a.prepare({resolve:(u)=>new URL(u,r).href,loadScript:D,loadStyle:C})}return S(e,I(e,a.default)),{name:e,status:"loaded"}}).catch((a)=>{if(m.get(e)?.promise===s)m.delete(e);throw a});return m.set(e,{url:r,promise:s}),s}async function q(e){let t=Object.entries(e),r=await Promise.allSettled(t.map(([s,a])=>F(s,a))),n={names:[],failed:[],skipped:[]};return r.forEach((s,a)=>{let u=t[a][0];if(s.status==="rejected")n.failed.push({name:u,error:s.reason});else if(s.value.status==="skipped")n.skipped.push(u);else n.names.push(u)}),n}function z(e){let t=JSON.parse(e);if(typeof t!=="object"||t===null||Array.isArray(t))throw TypeError("Manifest must be a JSON object.");return Object.entries(t)}async function P(e){let t=z(e.textContent),r=await q(Object.fromEntries(t));for(let n of r.failed)console.error(`Enhancement "${n.name}" failed to load:`,n.error);if(r.failed.length===0)e.remove();return r}function x(){return E({[L]:(e)=>{P(e).catch((t)=>{console.error("Failed to parse enhancement manifest:",t)})}})}x();
1
+ var g=new WeakMap,w=new WeakMap;function b(){return{refresh(){},disconnect(){}}}function N(e){return e?.nodeType===1||e?.nodeType===9||e?.nodeType===11}function A(e){let n=new Set,r="";function t(o){try{if(e.querySelector?.(o),e.nodeType===1)e.matches(o);return!0}catch(s){return console.error(`Invalid enhancer selector "${o}"`,s),!1}}function c(){r=[...n].flatMap((o)=>o.selectors).join(",")}function a(o,s){if(s?.nodeType!==1)return;let f=o.instances.get(s);for(let u of o.selectors){if(!s.matches(u))continue;if(f?.has(u))continue;let p;try{p=o.enhancers[u](s)}catch(v){console.error(`Enhancer failed for selector "${u}"`,v);continue}if(!f)f=new Map,o.instances.set(s,f);f.set(u,typeof p==="function"?p:null)}}function l(o,s){let f=o.instances.get(s);if(!f)return;for(let u of f.values())try{u?.()}catch(p){console.error("Enhancer cleanup failed",p)}o.instances.delete(s)}function i(o,s){if(!N(s))return;if(s.nodeType===1)a(o,s);if(o.selectorString)s.querySelectorAll?.(o.selectorString).forEach((f)=>{a(o,f)})}function d(o){if(!N(o)||!r)return;if(o.nodeType===1)for(let s of n)a(s,o);o.querySelectorAll?.(r).forEach((s)=>{for(let f of n)a(f,s)})}function y(){for(let o of n)for(let s of Array.from(o.instances.keys()))if(!s.isConnected||!e.contains(s))l(o,s)}let h=new MutationObserver((o)=>{let s=!1;for(let f of o){for(let u of f.addedNodes)d(u);if(f.removedNodes.length>0)s=!0}if(s)y()});return{add(o){let s=Object.keys(o).filter(t);if(!s.length)return null;let u={disconnected:!1,enhancers:Object.fromEntries(s.map((p)=>[p,o[p]])),instances:new Map,selectors:s,selectorString:s.join(",")};if(n.add(u),n.size===1)h.observe(e,{childList:!0,subtree:!0});return c(),i(u,e),u},refresh(o,s){if(o.disconnected)return;i(o,s)},remove(o){if(o.disconnected)return;o.disconnected=!0;for(let s of Array.from(o.instances.keys()))l(o,s);if(n.delete(o),c(),n.size===0)h.disconnect(),g.delete(e)}}}var _=/^[a-z][a-z0-9-]*$/;function M(e){if(!_.test(e))throw TypeError(`Invalid enhancement name: ${e}`);return`[data-enhance~="${e}"]`}function T(e,n,r){if(typeof document>"u")return b();if(M(e),typeof n!=="function")throw TypeError("registerEnhancement() requires a function init.");r??=document.documentElement;let t=w.get(r);if(!t)t=new Map,w.set(r,t);if(t.has(e))throw Error(`Enhancement "${e}" is already registered on this root.`);let c=E({[M(e)]:n},r);return t.set(e,c),{refresh:(a)=>c.refresh(a),disconnect:()=>{if(c.disconnect(),t.get(e)===c)t.delete(e);if(t.size===0)w.delete(r)}}}function U(e){let n=g.get(e);if(!n)n=A(e),g.set(e,n);return n}function E(e,n){if(typeof document>"u")return b();n??=document.documentElement;let r=U(n),t=r.add(e);if(!t)return b();return{refresh:(c)=>r.refresh(t,c),disconnect:()=>{r.remove(t)}}}function D(e){return e.replace(/([a-zA-Z_$][\w$-]*)\s*:|'((?:\\'|[^'])*)'/g,(n,r,t)=>{if(r)return`"${r}":`;return`"${t.replace(/\\'/g,"'").replace(/"/g,"\\\"")}"`})}function O(e){if(e==null||typeof e!=="string")return{};let n=e.trim();if(n==="")return{};let r=n,t;try{t=JSON.parse(r)}catch{let c=D(r);r=c.startsWith("{")?c:`{${c}}`,t=JSON.parse(r)}if(t===null||typeof t!=="object"||Array.isArray(t))throw TypeError("Enhancement config must be an object");return t}var R='script[type="application/json"][data-enhance-modules]',m=new Map,L=new Map,j=new Map,C=(e)=>import(e);function S(e,n,r){let t=new URL(e,document.baseURI).href;if(!n.has(t)){let c=new Promise((a,l)=>{let i=document.createElement(r==="script"?"script":"link");if(r==="script")i.src=t;else i.rel="stylesheet",i.href=t;i.onload=a,i.onerror=()=>{i.remove(),l(Error(`Unable to load ${r}: ${t}`))},document.head.append(i)});n.set(t,c),c.catch(()=>{if(n.get(t)===c)n.delete(t)})}return n.get(t)}function k(e){return S(e,L,"script")}function I(e){return S(e,j,"style")}function J(e,n){return(r)=>{let t=new AbortController,c=r.getAttribute("data-enhance-config"),a=O(c??""),l=(d,y,h={})=>r.dispatchEvent(new CustomEvent(`actual:${e}:${d}`,{bubbles:!0,detail:y,...h})),i;try{i=n(r,{config:a,signal:t.signal,emit:l})}catch(d){throw t.abort(),d}if(i?.then)throw t.abort(),TypeError(`Enhancement "${e}" returned a Promise. connect() must be synchronous; use prepare() for asynchronous setup.`);if(i!==void 0&&typeof i!=="function")throw t.abort(),TypeError(`Enhancement "${e}" must return a cleanup function or undefined.`);return()=>{t.abort(),i?.()}}}function q(e,n){if(typeof n!=="string"||n.trim()==="")throw TypeError(`Enhancement "${e}" must declare a non-empty module URL.`)}function z(e,n){q(e,n);let r=new URL(n,document.baseURI).href,t=m.get(e);if(t){if(t.url!==r)throw Error(`Enhancement "${e}" is already declared from ${t.url} and cannot be replaced by ${r}.`);return t.promise.then(()=>({name:e,status:"skipped"}))}let c=C(r).then(async(a)=>{if(typeof a.default!=="function")throw TypeError(`Enhancement "${e}" must export a default function.`);if(a.prepare!==void 0){if(typeof a.prepare!=="function")throw TypeError(`Enhancement "${e}" exports an invalid prepare().`);await a.prepare({resolve:(l)=>new URL(l,r).href,loadScript:k,loadStyle:I})}return T(e,J(e,a.default)),{name:e,status:"loaded"}}).catch((a)=>{if(m.get(e)?.promise===c)m.delete(e);throw a});return m.set(e,{url:r,promise:c}),c}async function F(e){let n=await Promise.allSettled(e.map(([t,c])=>z(t,c))),r={names:[],failed:[],skipped:[]};return n.forEach((t,c)=>{let a=e[c][0];if(t.status==="rejected")r.failed.push({name:a,error:t.reason});else if(t.value.status==="skipped")r.skipped.push(a);else r.names.push(a)}),r}function P(e){let n=JSON.parse(e);if(typeof n!=="object"||n===null||Array.isArray(n))throw TypeError("Manifest must be a JSON object.");return Object.entries(n)}async function W(e){let n=P(e.textContent),r=await F(n);for(let t of r.failed)console.error(`Enhancement "${t.name}" failed to load:`,t.error);if(r.failed.length===0)e.remove();return r}function x(){return E({[R]:(e)=>{W(e).catch((n)=>{console.error("Failed to parse enhancement manifest:",n)})}})}x();
@@ -1 +1 @@
1
- *, *::before, *::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } body { margin: 0; } button, input, textarea, select { font: inherit; } [hidden]:not([hidden="until-found"]) { display: none !important; } :where(img, svg, video) { max-inline-size: 100%; block-size: auto; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto !important; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0s !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; transition-delay: 0s !important; } } :root { --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --line-height: 1.5; --line-height-tight: 1.25; --line-height-relaxed: 1.75; --font-weight: 400; --font-weight-medium: 500; --font-weight-strong: 650; --font-weight-bold: 700; --font-size-xs: 0.8125rem; --font-size-sm: 0.875rem; --font-size-md: 1rem; --font-size-lg: 1.125rem; --gap: 0.75rem; --density-space: var(--space-40); --density-compact-size: 1.5rem; --space-10: 0.25rem; --space-20: 0.5rem; --space-30: 0.75rem; --space-40: 1rem; --space-50: 1.5rem; --space-60: 2rem; --border-width: 1px; --radius-full: 9999px; --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04); --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15); --duration: 150ms; --duration-fast: 100ms; --duration-slow: 200ms; --duration-spin: 0.75s; --duration-shimmer: 1.2s; --ease-enter: cubic-bezier(0.2, 0, 0, 1); --ease-exit: cubic-bezier(0.4, 0, 1, 1); --focus-outline-color: currentColor; --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color); --focus-outline-offset: 2px; --focus-ring-width: 3px; --indicator-offset: 14.65%; --backdrop-color: rgb(0 0 0); --backdrop-opacity: 0.45; --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity)); --bar-height: 0.65rem; --control-size-sm: 2rem; --control-size-md: 2.375rem; --control-size-lg: 2.75rem; --control-size: var(--control-size-md); --control-pad-x: 1em; --control-font-size: var(--font-size-md); --choice-control-size: 1.125em; --disabled-opacity: 0.65; } :root, [data-theme] { --state-selected: var(--primary); --state-selected-fg: var(--primary-fg); --state-disabled: var(--text-subtle); --indicator-ring: var(--surface); } :root { --soft-bg-mix: 88%; --soft-border-mix: 65%; --soft-hover-alpha: 12%; --soft-fg-mix: 100%; --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E"); --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E"); --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E"); --viewport-block: 100vh; --viewport-inline: 100vw; --z-sticky: 10; --z-menu: 20; --z-tooltip: 50; --z-status: 60; } @media (forced-colors: active) { :root, [data-theme] { --state-disabled: GrayText; --disabled-opacity: 1; } } @supports (block-size: 100dvh) { :root { --viewport-block: 100dvh; } } @supports (inline-size: 100vi) { :root { --viewport-inline: 100vi; } } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --shadow: 0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent), 0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent); --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent); } :root { --backdrop-fill: color-mix( in oklch, var(--backdrop-color) calc(var(--backdrop-opacity) * 100%), transparent ); } } :root { color-scheme: light dark; } [data-theme="light"] { color-scheme: light; } [data-theme="dark"] { color-scheme: dark; } @supports not (color: light-dark(red, blue)) { :root, [data-theme="light"], [data-theme="dark"] { color-scheme: light; } } :root, [data-theme="light"] { --primary: hsl(268 32% 33%); --primary-fg: hsl(0 0% 98%); --secondary: hsl(16 38% 38%); --secondary-fg: hsl(0 0% 98%); --success: hsl(150 40% 31%); --success-fg: hsl(0 0% 98%); --warning: hsl(36 68% 34%); --warning-fg: hsl(0 0% 98%); --danger: hsl(5 58% 43%); --danger-fg: hsl(0 0% 98%); --neutral: hsl(258 7% 43%); --neutral-fg: hsl(0 0% 98%); --surface: hsl(0 0% 100%); --surface-raised: hsl(0 0% 100%); --surface-subtle: hsl(262 24% 96%); --surface-solid: hsl(261 16% 13%); --text: hsl(261 20% 13%); --text-muted: hsl(259 9% 42%); --text-subtle: hsl(258 7% 57%); --border: hsl(260 15% 88%); --focus: var(--neutral); --focus-ring: hsl(258 7% 43% / 0.3); --hover-overlay: hsl(260 20% 10% / 0.04); --radius: 0.5rem; --radius-sm: 0.25rem; --radius-lg: 0.75rem; --shadow-color: hsl(260 25% 10%); --hgroup-gap: 0.25em; } :root, [data-theme] { --heading: var(--text); --selection-bg: var(--primary); --selection-fg: var(--primary-fg); --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring); } :root, [data-theme] { accent-color: var(--primary); } [data-tone="dark"] { --tone-fg: white; } [data-tone="light"] { --tone-fg: var(--text); } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent); } } @supports (color: light-dark(red, blue)) { :root, [data-theme="light"] { --primary: light-dark(hsl(268 32% 33%), hsl(268 42% 76%)); --primary-fg: light-dark(hsl(0 0% 98%), hsl(268 30% 12%)); --secondary: light-dark(hsl(16 38% 38%), hsl(18 45% 72%)); --secondary-fg: light-dark(hsl(0 0% 98%), hsl(18 32% 11%)); --success: light-dark(hsl(150 40% 31%), hsl(150 38% 62%)); --success-fg: light-dark(hsl(0 0% 98%), hsl(150 35% 8%)); --warning: light-dark(hsl(36 68% 34%), hsl(40 62% 64%)); --warning-fg: light-dark(hsl(0 0% 98%), hsl(38 35% 10%)); --danger: light-dark(hsl(5 58% 43%), hsl(4 60% 70%)); --danger-fg: light-dark(hsl(0 0% 98%), hsl(4 35% 10%)); --neutral: light-dark(hsl(258 7% 43%), hsl(258 10% 72%)); --neutral-fg: light-dark(hsl(0 0% 98%), hsl(260 18% 10%)); --surface: light-dark(hsl(0 0% 100%), hsl(263 17% 9%)); --surface-raised: light-dark(hsl(0 0% 100%), hsl(263 15% 12%)); --surface-subtle: light-dark(hsl(262 24% 96%), hsl(263 13% 16%)); --surface-solid: light-dark(hsl(261 16% 13%), hsl(0 0% 96%)); --text: light-dark(hsl(261 20% 13%), hsl(260 12% 95%)); --text-muted: light-dark(hsl(259 9% 42%), hsl(259 9% 67%)); --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%)); --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%)); --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28)); --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06)); --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%)); } } @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) { :root, [data-theme] { --focus-ring: light-dark( color-mix(in oklch, var(--focus) 30%, transparent), color-mix(in oklch, var(--focus) 28%, transparent) ); } } @media (prefers-contrast: more) { :root, [data-theme][data-theme] { --border-width: 2px; --focus-outline-offset: 3px; --focus-ring-width: 4px; --text-muted: var(--text); --text-subtle: var(--text); } } body { background: var(--surface); color: var(--text); font-family: var(--font-sans); line-height: var(--line-height); } a { color: var(--link, inherit); text-underline-offset: 0.18em; } a:hover { text-decoration-thickness: 0.12em; } :where(h1, h2, h3, h4, h5, h6) { color: var(--heading, inherit); line-height: var(--line-height-tight); text-wrap: balance; } hgroup { display: grid; gap: var(--hgroup-gap); } hgroup > * { margin-block: 0; } hr { border: 0; border-block-start: var(--border-width) solid var(--border); margin-block: var(--space-60); } code, kbd, samp, pre { font-family: var(--font-mono); } small { font-size: 0.875em; } ::selection { background: var(--selection-bg, var(--primary)); color: var(--selection-fg, var(--primary-fg)); } .primary { --intent: var(--primary); --intent-fg: var(--primary-fg); } .secondary { --intent: var(--secondary); --intent-fg: var(--secondary-fg); } .success { --intent: var(--success); --intent-fg: var(--success-fg); } .warning { --intent: var(--warning); --intent-fg: var(--warning-fg); } .danger { --intent: var(--danger); --intent-fg: var(--danger-fg); } .neutral { --intent: var(--neutral); --intent-fg: var(--neutral-fg); } .intent-color { color: var(--intent); } .solid { --ui-bg: var(--intent, var(--neutral)); --ui-fg: var(--intent-fg, var(--neutral-fg)); --ui-border: transparent; } .soft { --ui-bg: var(--surface-subtle); --ui-border: var(--border); --ui-fg: var(--intent, var(--text)); } .outline { --ui-bg: transparent; --ui-fg: var(--intent, currentColor); --ui-border: var(--intent, currentColor); } .surface { --ui-bg: var(--surface); --ui-fg: var(--intent, var(--text)); --ui-border: var(--border); } .inverted { --ui-bg: var(--surface-solid); --ui-fg: var(--surface); --ui-border: transparent; --ui-hover-bg: var(--surface-solid); --heading: var(--surface); --busy-overlay-bg: var(--surface-solid); background: var(--ui-bg); color: var(--ui-fg); border-color: var(--ui-border); } .sm { --gap: var(--space-20); --density-space: var(--space-20); --control-size: var(--control-size-sm); --density-compact-size: 1.25rem; } .lg { --gap: var(--space-50); --density-space: var(--space-50); --control-size: var(--control-size-lg); --density-compact-size: 1.875rem; } @supports (color: color-mix(in oklch, red, white)) { .soft { --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral))); --ui-border: color-mix( in oklab, var(--surface) var(--soft-border-mix), var(--intent, var(--neutral)) ); --ui-fg: color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix)); } .inverted { --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black); } } :focus { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); } @supports selector(:focus-visible) { :focus:not(:focus-visible) { outline: none; } } @media print { :root, body { color-scheme: light; background: #fff; color: #000; } h1, h2, h3, h4, h5, h6 { break-after: avoid; } }
1
+ *, *::before, *::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } body { margin: 0; } button, input, textarea, select { font: inherit; } [hidden]:not([hidden="until-found"]) { display: none !important; } :where(img, svg, video) { max-inline-size: 100%; block-size: auto; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto !important; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0s !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; transition-delay: 0s !important; } } :root { --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --line-height: 1.5; --line-height-tight: 1.25; --line-height-relaxed: 1.75; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; --font-size-xs: 0.8125rem; --font-size-sm: 0.875rem; --font-size-md: 1rem; --font-size-lg: 1.125rem; --gap: 0.75rem; --density-space: var(--space-40); --space-10: 0.25rem; --space-20: 0.5rem; --space-30: 0.75rem; --space-40: 1rem; --space-50: 1.5rem; --space-60: 2rem; --border-width: 1px; --radius-full: 9999px; --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04); --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15); --duration: 150ms; --duration-fast: 100ms; --duration-slow: 200ms; --duration-spin: 0.75s; --duration-shimmer: 1.2s; --ease-enter: cubic-bezier(0.2, 0, 0, 1); --ease-exit: cubic-bezier(0.4, 0, 1, 1); --focus-outline-color: currentColor; --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color); --focus-outline-offset: 2px; --focus-ring-width: 3px; --indicator-offset: 14.65%; --backdrop-color: rgb(0 0 0); --backdrop-opacity: 0.45; --backdrop-fill: rgb(0 0 0 / var(--backdrop-opacity)); --bar-height: 0.65rem; --control-size-sm: 2rem; --control-size-md: 2.375rem; --control-size-lg: 2.75rem; --control-size: var(--control-size-md); --control-pad-x: var(--font-size-md); --control-font-size: var(--font-size-md); --choice-control-size: 1.125em; --disabled-opacity: 0.65; } :root, [data-theme] { --state-selected: var(--primary); --state-selected-fg: var(--primary-fg); --state-disabled: var(--text-subtle); --indicator-ring: var(--surface); } :root { --soft-bg-mix: 88%; --soft-border-mix: 65%; --soft-hover-alpha: 12%; --soft-fg-mix: 100%; --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E"); --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E"); --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E"); --viewport-block: 100vh; --viewport-inline: 100vw; --z-sticky: 10; --z-menu: 20; --z-status: 60; } @media (forced-colors: active) { :root, [data-theme] { --state-disabled: GrayText; --disabled-opacity: 1; } } @supports (block-size: 100dvh) { :root { --viewport-block: 100dvh; } } @supports (inline-size: 100vi) { :root { --viewport-inline: 100vi; } } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --shadow: 0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent), 0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent); --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent); } :root, [data-theme] { --backdrop-fill: color-mix( in oklch, var(--backdrop-color) calc(var(--backdrop-opacity) * 100%), transparent ); } } :root { color-scheme: light dark; } [data-theme="light"] { color-scheme: light; } [data-theme="dark"] { color-scheme: dark; } @supports not (color: light-dark(red, blue)) { :root, [data-theme="light"], [data-theme="dark"] { color-scheme: light; } } :root, [data-theme="light"] { --primary: hsl(268 32% 33%); --primary-fg: hsl(0 0% 98%); --secondary: hsl(16 38% 38%); --secondary-fg: hsl(0 0% 98%); --success: hsl(150 40% 31%); --success-fg: hsl(0 0% 98%); --warning: hsl(36 68% 34%); --warning-fg: hsl(0 0% 98%); --danger: hsl(5 58% 43%); --danger-fg: hsl(0 0% 98%); --neutral: hsl(258 7% 43%); --neutral-fg: hsl(0 0% 98%); --surface: hsl(0 0% 100%); --surface-raised: hsl(0 0% 100%); --surface-subtle: hsl(262 24% 96%); --surface-solid: hsl(261 16% 13%); --text: hsl(261 20% 13%); --text-muted: hsl(259 9% 42%); --text-subtle: hsl(258 7% 57%); --border: hsl(260 15% 88%); --focus: var(--neutral); --focus-ring: hsl(258 7% 43% / 0.3); --hover-overlay: hsl(260 20% 10% / 0.04); --radius: 0.5rem; --radius-sm: 0.25rem; --radius-lg: 0.75rem; --shadow-color: hsl(260 25% 10%); --hgroup-gap: 0.25em; } :root, [data-theme] { --heading: var(--text); --selection-bg: var(--primary); --selection-fg: var(--primary-fg); --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring); } :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) { --primary-soft-fg: initial; --secondary-soft-fg: initial; --success-soft-fg: initial; --warning-soft-fg: initial; --danger-soft-fg: initial; --neutral-soft-fg: initial; } :root, [data-theme="light"], [data-theme="dark"] { --secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%); --success-soft-fg: color-mix(in oklab, var(--text) 15%, var(--success) 85%); --warning-soft-fg: color-mix(in oklab, var(--text) 25%, var(--warning) 75%); --danger-soft-fg: color-mix(in oklab, var(--text) 15%, var(--danger) 85%); --neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%); } :root, [data-theme] { accent-color: var(--primary); } [data-tone="dark"] { --tone-fg: white; } [data-tone="light"] { --tone-fg: var(--text); } @supports (color: color-mix(in oklch, red, white)) { :root, [data-theme] { --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent); } } @supports (color: light-dark(red, blue)) { :root, [data-theme="light"] { --primary: light-dark(hsl(268 32% 33%), hsl(268 42% 76%)); --primary-fg: light-dark(hsl(0 0% 98%), hsl(268 30% 12%)); --secondary: light-dark(hsl(16 38% 38%), hsl(18 45% 72%)); --secondary-fg: light-dark(hsl(0 0% 98%), hsl(18 32% 11%)); --success: light-dark(hsl(150 40% 31%), hsl(150 38% 62%)); --success-fg: light-dark(hsl(0 0% 98%), hsl(150 35% 8%)); --warning: light-dark(hsl(36 68% 34%), hsl(40 62% 64%)); --warning-fg: light-dark(hsl(0 0% 98%), hsl(38 35% 10%)); --danger: light-dark(hsl(5 58% 43%), hsl(4 60% 70%)); --danger-fg: light-dark(hsl(0 0% 98%), hsl(4 35% 10%)); --neutral: light-dark(hsl(258 7% 43%), hsl(258 10% 72%)); --neutral-fg: light-dark(hsl(0 0% 98%), hsl(260 18% 10%)); --surface: light-dark(hsl(0 0% 100%), hsl(263 17% 9%)); --surface-raised: light-dark(hsl(0 0% 100%), hsl(263 15% 12%)); --surface-subtle: light-dark(hsl(262 24% 96%), hsl(263 13% 16%)); --surface-solid: light-dark(hsl(261 16% 13%), hsl(0 0% 96%)); --text: light-dark(hsl(261 20% 13%), hsl(260 12% 95%)); --text-muted: light-dark(hsl(259 9% 42%), hsl(259 9% 67%)); --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%)); --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%)); --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28)); --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06)); --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%)); } } @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) { :root, [data-theme] { --focus-ring: light-dark( color-mix(in oklch, var(--focus) 30%, transparent), color-mix(in oklch, var(--focus) 28%, transparent) ); } } @media (prefers-contrast: more) { :root, [data-theme][data-theme] { --border-width: 2px; --focus-outline-offset: 3px; --focus-ring-width: 4px; --text-muted: var(--text); --text-subtle: var(--text); } } body { background: var(--surface); color: var(--text); font-family: var(--font-sans); line-height: var(--line-height); } a { color: var(--link, inherit); text-underline-offset: 0.18em; } a:hover { text-decoration-thickness: 0.12em; } :where(h1, h2, h3, h4, h5, h6) { color: var(--heading, inherit); line-height: var(--line-height-tight); text-wrap: balance; } hgroup { display: grid; gap: var(--hgroup-gap); } hgroup > * { margin-block: 0; } hr { border: 0; border-block-start: var(--border-width) solid var(--border); margin-block: var(--hr-space, var(--space-60)); margin-inline: 0; } code, kbd, samp, pre { font-family: var(--font-mono); } pre { overflow-x: auto; } small { font-size: 0.875em; } ::selection { background: var(--selection-bg, var(--primary)); color: var(--selection-fg, var(--primary-fg)); } .primary { --intent: var(--primary); --intent-fg: var(--primary-fg); --intent-soft-fg: var(--primary-soft-fg); } .secondary { --intent: var(--secondary); --intent-fg: var(--secondary-fg); --intent-soft-fg: var(--secondary-soft-fg); } .success { --intent: var(--success); --intent-fg: var(--success-fg); --intent-soft-fg: var(--success-soft-fg); } .warning { --intent: var(--warning); --intent-fg: var(--warning-fg); --intent-soft-fg: var(--warning-soft-fg); } .danger { --intent: var(--danger); --intent-fg: var(--danger-fg); --intent-soft-fg: var(--danger-soft-fg); } .neutral { --intent: var(--neutral); --intent-fg: var(--neutral-fg); --intent-soft-fg: var(--neutral-soft-fg); } .intent-color { color: var(--intent); } .solid { --ui-bg: var(--intent, var(--neutral)); --ui-fg: var(--intent-fg, var(--neutral-fg)); --ui-border: transparent; } .soft { --ui-bg: var(--surface-subtle); --ui-border: var(--border); --ui-fg: var(--intent, var(--text)); } .outline { --ui-bg: transparent; --ui-fg: var(--intent, currentColor); --ui-border: var(--intent, currentColor); } .surface { --ui-bg: var(--surface); --ui-fg: var(--intent, var(--text)); --ui-border: var(--border); } .inverted { --ui-bg: var(--surface-solid); --ui-fg: var(--surface); --ui-border: transparent; --ui-hover-bg: var(--surface-solid); --heading: var(--surface); --busy-overlay-bg: var(--surface-solid); background: var(--ui-bg); color: var(--ui-fg); border-color: var(--ui-border); } .compact { --gap: var(--space-20); --density-space: var(--space-20); --control-size: var(--control-size-sm); } .spacious { --gap: var(--space-50); --density-space: var(--space-50); --control-size: var(--control-size-lg); } :where( .btn, .input, .textarea, .select, .color, .file, .field, .choice, .input-icon, .otp, .join, .pagination, .tabs, .tab, .flyout ).sm { --control-size: var(--control-size-sm); --control-font-size: var(--font-size-sm); --control-pad-x: var(--font-size-sm); } :where( .btn, .input, .textarea, .select, .color, .file, .field, .choice, .input-icon, .otp, .join, .pagination, .tabs, .tab, .flyout ).lg { --control-size: var(--control-size-lg); --control-font-size: var(--font-size-lg); --control-pad-x: var(--font-size-lg); } @supports (color: color-mix(in oklch, red, white)) { .soft { --ui-bg: color-mix(in oklab, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral))); --ui-border: color-mix( in oklab, var(--surface) var(--soft-border-mix), var(--intent, var(--neutral)) ); --ui-fg: var( --intent-soft-fg, color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix)) ); } .inverted { --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black); } } :focus { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); } @supports selector(:focus-visible) { :focus:not(:focus-visible) { outline: none; } } @media print { :root, body { color-scheme: light; background: #fff; color: #000; } h1, h2, h3, h4, h5, h6 { break-after: avoid; } }
@@ -10,6 +10,39 @@ Actual CSS makes the accessible path the default where CSS and small runtime enh
10
10
  - Forced-colors support on controls and visual state indicators.
11
11
  - DOM tests for enhancer lifecycle, focus, attributes, and keyboard behavior.
12
12
 
13
+ ## Linters that reject ARIA patterns
14
+
15
+ Measured on Biome 2.5.6, in HTML and JSX alike.
16
+
17
+ `a11y/noNoninteractiveElementToInteractiveRole` reports `<menu role="menu">`,
18
+ and its fix removes the role, which dismantles the composite. It is a port of
19
+ `jsx-a11y/no-noninteractive-element-to-interactive-role` that dropped the source
20
+ rule's default allowlist, so every ARIA composite built on a list is reported —
21
+ `menu`, `listbox`, `tablist`, `menubar`, `tree`, `grid` — even though the source
22
+ documents `<ul role="menu">` as valid. Switch the rule off; a per-element
23
+ suppression would sit above every menu in the markup.
24
+
25
+ `a11y/useSemanticElements` reports `role="group"` on `.join` and
26
+ `.avatar-stack`, suggesting `<fieldset>`. `<fieldset>` groups form controls
27
+ under a `<legend>`; ARIA's `group` is any labelled set of related objects. This
28
+ rule stays on — it correctly catches `<div role="navigation">`,
29
+ `<div role="button">`, `<div role="separator">` — so suppress it on the wrapper
30
+ instead. A wrapper with no label to give should drop the role rather than
31
+ suppress the rule.
32
+
33
+ Two decisions these findings settled:
34
+
35
+ - `.join` keeps `role="group"` rather than `role="toolbar"`, which the rule does
36
+ not report. Toolbar owes arrow-key navigation with a roving tabindex, and
37
+ claiming the role without the behaviour is worse than the false positive.
38
+ - `.menu-separator` is a plain `<hr>`. The `role="separator"` these docs used to
39
+ show is implied by the element, and `a11y/noRedundantRoles` was right to
40
+ reject it. Nothing in the CSS or the runtime read it.
41
+
42
+ Only the menu container is ever reported: `role="menuitem"` sits on a `<button>`
43
+ or `<a>`, never on the `<li>`, and an interactive element is a legitimate role
44
+ host. Tabs put `role="tablist"` on a `<div>`, so they are unaffected.
45
+
13
46
  ## Author Responsibilities
14
47
 
15
48
  - Name icon-only buttons.
@@ -5,12 +5,11 @@
5
5
  The compatibility contract is expressed in browser versions, not ECMAScript
6
6
  edition labels. Each tier is defined by what it guarantees.
7
7
 
8
- | Actual tier | Firefox | Safari | Chromium | Contract |
9
- | ------------ | ------: | --------: | -------: | ---------------------------------------------- |
10
- | Degraded | 78+ | 14+ | 88+ | Semantic HTML + core CSS; JS unsupported |
11
- | **Minimal** | **98+** | **15.4+** | **99+** | Full supported Actual experience, including JS |
12
- | Intermediate | 121+ | 16+ | 106+ | More modern platform enhancements |
13
- | Recommended | 129+ | 17.5+ | 123+ | Target experience |
8
+ | Actual tier | Firefox | Safari | Chromium | Contract |
9
+ | ----------- | -------: | ------: | -------: | ---------------------------------------------- |
10
+ | Degraded | 78+ | 14+ | 88+ | Semantic HTML + core CSS; JS unsupported |
11
+ | **Minimal** | **125+** | **17+** | **116+** | Full supported Actual experience, including JS |
12
+ | Recommended | 129+ | 17.5+ | 123+ | Target experience |
14
13
 
15
14
  - **Degraded** keeps useful HTML and CSS on older browsers. JavaScript is
16
15
  explicitly outside the supported contract there.
@@ -19,33 +18,43 @@ edition labels. Each tier is defined by what it guarantees.
19
18
  modern platform primitives available across the whole tier and ships no
20
19
  legacy compatibility layer or polyfill.
21
20
 
21
+ Only the JavaScript floor is expressed by Minimal. CSS degradation is owed to
22
+ Degraded, so a capability being available across Minimal does not make it CSS
23
+ baseline — `check:compat` audits stylesheets against the Degraded floor.
24
+
22
25
  Degraded browsers may still run some JavaScript successfully, but that
23
26
  behavior is not tested, documented as supported, or preserved when the
24
27
  runtime evolves.
25
28
 
26
29
  ### Native Popover and the tiers
27
30
 
28
- Popover is not part of any Actual tier's guarantee below Recommended. It is a
29
- capability, not a browser generation: the core lifecycle landed around
30
- Chromium 116, Firefox 125 and Safari 17, which clears Recommended but sits
31
- above both Minimal and Intermediate on every engine.
31
+ Minimal sits exactly where the manual Popover transport lands: Chromium 116,
32
+ Firefox 125, Safari 17. That is what the floor buys, and the whole of what the
33
+ runtime requires:
34
+
35
+ ```text
36
+ popover="manual"
37
+ showPopover()
38
+ hidePopover()
39
+ top-layer promotion + closed-state display: none
40
+ ```
32
41
 
33
- | Actual tier | Core native Popover guaranteed | Recommended lifecycle owner |
34
- | ------------ | ------------------------------ | ------------------------------- |
35
- | Degraded | no | HTML/CSS fallback only |
36
- | Minimal | no | `surface.js` |
37
- | Intermediate | no | `surface.js` |
38
- | Recommended | yes, core lifecycle | native Popover, or `surface.js` |
42
+ `popovertarget`, `popover="auto"` and `:popover-open` arrive on the same
43
+ engines and are guaranteed too, but the runtime uses none of them — it keeps
44
+ `aria-controls` and drives the lifecycle itself. Newer parts of the API
45
+ (`popover="hint"`, interest invokers) are covered by no tier.
39
46
 
40
- "Core lifecycle" means `popover="auto"`/`"manual"`, `popovertarget`,
41
- `showPopover`/`hidePopover` and `:popover-open` — not the entire Popover API.
42
- `popover="hint"` and interest invokers are much newer and are not covered at
43
- any tier. Safari coverage here is read as the desktop engine; some iOS versions
44
- through 18.2 still report partial support for parts of the API, so an
45
- application targeting iOS should verify the specific pieces it relies on.
47
+ | Actual tier | Manual Popover transport | Lifecycle owner |
48
+ | ----------- | ------------------------ | -------------------------------------------- |
49
+ | Degraded | no | HTML/CSS fallback only |
50
+ | Minimal | yes | `surface.js`, `tooltip.js` |
51
+ | Recommended | yes | native Popover, or `surface.js`/`tooltip.js` |
46
52
 
47
- Actual's presentation classes compose with either lifecycle from Recommended
48
- up. The runtime itself depends on Popover at no tier.
53
+ Actual's presentation classes compose with either lifecycle. Both runtimes use
54
+ the transport and never the native lifecycle: `popover="auto"` cannot express
55
+ `data-flyout-auto-close`, and it would put the browser's light dismiss in
56
+ competition with the tooltip's own hover and focus policy. See
57
+ [platform-alignment](platform-alignment.md).
49
58
 
50
59
  ## JavaScript philosophy
51
60
 
@@ -60,9 +69,9 @@ JavaScript support below Minimal.
60
69
 
61
70
  ### Abortable listeners
62
71
 
63
- Abortable event listeners predate the Minimal floor: Firefox 86, Chrome 88,
64
- and WebKit (early 2021) all shipped the capability before the Safari 15.4
65
- threshold. ([Bugzilla][1], [Chrome][2], [WebKit][3])
72
+ Abortable event listeners predate the Minimal floor by a wide margin: Firefox
73
+ 86, Chrome 88, and WebKit (early 2021). ([Bugzilla][1], [Chrome][2],
74
+ [WebKit][3])
66
75
 
67
76
  > `addEventListener({ signal })` is a baseline runtime primitive.
68
77
 
@@ -72,11 +81,25 @@ that is already native throughout Minimal.
72
81
 
73
82
  ### Native `<dialog>`
74
83
 
75
- The Minimal floor sits at the point where Safari 15.4 and Firefox 98 ship
76
- native `<dialog>`, and Chromium already had it. ([WebKit][4])
84
+ Native `<dialog>` predates the floor on every engine: Safari 15.4, Firefox 98,
85
+ and Chromium earlier still. ([WebKit][4])
77
86
 
78
87
  > Native dialog is assumed by the runtime; no dialog polyfill is shipped.
79
88
 
89
+ ### The manual Popover transport
90
+
91
+ The floor is set by this capability. `surface.js` and `tooltip.js` both promote
92
+ their element with `popover="manual"` instead of moving it in the DOM, which is
93
+ what keeps a surface — and an explicit tooltip — inside every scope that reaches
94
+ it by inheritance. See [surface-reparenting](surface-reparenting.md).
95
+
96
+ > The manual Popover transport is assumed by the runtime; there is no
97
+ > reparenting fallback.
98
+
99
+ A capability branch was rejected rather than overlooked: the fallback path is
100
+ the defect, so keeping it would leave the bug in place for exactly the browsers
101
+ the branch would have served.
102
+
80
103
  ### Future-direction invariants
81
104
 
82
105
  1. **Minimal drives JavaScript compatibility; Degraded does not.**
@@ -120,7 +143,7 @@ different notion from an Actual tier.
120
143
  | `appearance: base-select` | 2024+ | Native `<select>` |
121
144
  | `@starting-style` | 2024+ | Instant appearance |
122
145
  | View Transition API | 2024+ | Dialog open/close transition |
123
- | Popover API (core) | Baseline 2025 | `surface.js` lifecycle |
146
+ | Popover transport (`manual`) | Baseline 2025 | none — runtime requirement |
124
147
  | CSS Anchor Positioning | Future | JS positioning (`@lekoala/floating`) |
125
148
 
126
149
  Core layout and visual tokens work on all Baseline 2023 browsers. Features marked
@@ -155,6 +178,15 @@ background: color-mix(in oklch, var(--surface) 88%, transparent);
155
178
  backdrop-filter: blur(1rem);
156
179
  ```
157
180
 
181
+ `@supports` is the simplest machine-readable boundary for a structural
182
+ enhancement, not a preferred shape in itself. Where an unsupported selector
183
+ already drops the enhancement cleanly and the baseline stays functional, a
184
+ documented why-comment (the one the guard audit recognises) or a `PROGRESSIVE`
185
+ ledger entry is just as valid. A wrapper that is behaviorally redundant is a
186
+ simplification candidate: improve `check:compat` — for example, let one
187
+ justification cover a contiguous group of rules — rather than let the wrapper
188
+ become a framework convention.
189
+
158
190
  ### CSS nesting inside @supports
159
191
 
160
192
  The no-nesting rule protects browsers that predate CSS nesting (Chrome 112, Firefox 117, Safari 16.5). A block already gated behind a later feature can't be reached by any browser that lacks nesting, so the inspector-friendliness argument no longer applies — nest freely inside it. `appearance: base-select` (Chrome 133+) is the current example: everything inside that `@supports` block in `custom-select.css` uses `&`-nesting instead of repeating the `.select:not(...)` selector.
@@ -153,11 +153,11 @@ It does **not** guarantee that arbitrary content will remain usable inside a ver
153
153
 
154
154
  A two-column span in a narrow container may still be too small for its content. That is an application composition problem, not something the primitive should guess how to solve.
155
155
 
156
- ## Gap utilities are exact here
156
+ ## Gap is not coupled to the track count
157
157
 
158
- `grid.css` documents a pitfall: `.gap-sm` and `.gap-lg` set the `gap` property without updating `--gap`, and the unwrapped `.grid-N` baseline sizes its tracks from `--gap`, so the two disagree and a bare `.grid-4.gap-lg` stops one column short.
158
+ A grid that sizes its tracks from `--gap` couples spacing and density: setting the `gap` property on its own can make the two disagree. Actual removed its `.gap-sm` / `.gap-md` / `.gap-lg` utilities before 0.8 for exactly that reason — a local gap is now a direct `gap` declaration, and `--gap` is reserved for rebinding an inherited rhythm.
159
159
 
160
- `.column-layout` has no such coupling. Its track template is `repeat(12, minmax(0, 1fr))` and never reads `--gap` for sizing, so `gap` and the track count are independent. Every gap utility is exact at every width.
160
+ `.column-layout` never had that coupling. Its track template is `repeat(12, minmax(0, 1fr))` and never reads `--gap` for sizing, so `gap` and the track count are independent at every width.
161
161
 
162
162
  This is worth stating explicitly because an author who has read the grid page will reasonably assume the opposite.
163
163
 
@@ -1,33 +1,87 @@
1
- # Future Transition: CSS Anchor Positioning
1
+ # CSS anchor positioning
2
2
 
3
- ## Context
3
+ Placement stays in JavaScript. This note records why, so the question is not
4
+ reopened on browser-support figures alone.
4
5
 
5
- Currently, the positioning logic for tooltips, flyouts, and context menus is handled by the `@lekoala/floating` JavaScript dependency. This engine computes fixed coordinates, handles viewport boundaries, and applies "flip" and "shift" heuristics to prevent overflow.
6
+ ## What owns placement today
6
7
 
7
- As of mid-2024, modern browsers started supporting the **CSS Anchor Positioning API**, which delegates this heavy mathematical lifting directly to the browser's layout engine.
8
+ `@lekoala/floating` positions every anchored surface. `surface.js` calls
9
+ `reposition()` for an element anchor and `repositionAt()` for a pointer
10
+ coordinate, both inside an `autoUpdate()` loop.
8
11
 
9
- ## Migration Path
12
+ The positioner is not only a placement engine. It writes outputs the components
13
+ read as ordinary CSS:
10
14
 
11
- When global browser support for CSS Anchor Positioning reaches a safe threshold (~95%), the JS positioning engine should be deprecated in favor of a CSS-only approach.
15
+ ```text
16
+ left / top the placement itself
17
+ data-placement resolved side, after flip
18
+ --arrow-x / --arrow-y arrow offset along the panel edge
19
+ --available-height space left for the panel
20
+ ```
21
+
22
+ `tooltip.css` draws its arrow from `data-placement` and `--arrow-x/-y`;
23
+ `flyout.css` caps its panel with `--available-height`. Replacing the engine
24
+ means replacing those, not only the coordinates.
12
25
 
13
- ### 1. Element-to-Element Anchoring (Tooltips & Flyouts)
26
+ ## The blocker is not support
14
27
 
15
- For elements anchored to physical DOM nodes (like a button triggering a flyout), the JS calculation can be fully replaced by CSS:
28
+ Support is the obvious gate and the wrong one. The blocking output is the one
29
+ that is not CSS at all — the positioner's return value:
16
30
 
17
- ```css
18
- .flyout {
19
- position: absolute;
20
- position-anchor: --trigger-element;
21
- top: anchor(bottom);
22
- left: anchor(start);
23
-
24
- /* Replaces the JS flip/shift logic natively */
25
- position-try-fallbacks: flip-block, flip-inline;
26
- }
31
+ ```js
32
+ if (!positionSurface(menu)) closeSurface(menu); // surface.js
27
33
  ```
28
34
 
29
- ### 2. The Pointer Coordinates Exception (Context Menus)
35
+ `reposition()` returns `false` when positioning cannot be performed: a hidden
36
+ floating element, a missing reference rect, an anchor outside the boundary, or
37
+ a document with no browsing context. `surface.js` runs that check on every
38
+ `autoUpdate` tick, so an anchor that leaves its boundary **closes** the surface.
39
+
40
+ CSS anchor positioning offers no equivalent signal. `position-try-fallbacks`
41
+ re-places the box and `position-visibility` can hide it, but hiding is not
42
+ closing: no event fires, `.is-open` stays set, focus is never restored, and the
43
+ Escape stack keeps an entry for a panel the user can no longer see.
44
+
45
+ This is the boundary the manual Popover transport was adopted on, in the other
46
+ direction — a capability takes one responsibility without being handed the
47
+ neighbouring one. See [platform-alignment](platform-alignment.md).
48
+
49
+ ## The pointer-coordinate case
50
+
51
+ Context menus open at `clientX`/`clientY`, not at a DOM element
52
+ (`context-menu.js` stores the point, `surface.js` passes it to
53
+ `repositionAt()`). Anchor positioning anchors to elements, so this path needs a
54
+ script layer whatever happens to the element-anchored one. Adoption would split
55
+ the positioner in two rather than delete it.
56
+
57
+ ## The one output with a declarative equivalent
58
+
59
+ `--surface-anchor-width` is measured from the trigger's rect on every
60
+ reposition, solely so a flyout can match its trigger's width. `anchor-size()`
61
+ expresses that relationship directly and would remove both the measurement and
62
+ the property.
63
+
64
+ It is not free either: the panel would need an `anchor-name` on its trigger and
65
+ a `position-anchor` written at runtime, which is the same plumbing the
66
+ positioner already does. Worth doing only alongside a wider move, not as a
67
+ standalone saving.
68
+
69
+ ## What would unblock the move
70
+
71
+ One of:
72
+
73
+ - a script-observable anchor-visibility signal, so the lifecycle keeps its
74
+ close decision while CSS takes the geometry; or
75
+ - a deliberate contract change — an out-of-view surface hides instead of
76
+ closing. That is a component decision, not a transport swap, and it has the
77
+ same shape as the `popover="auto"` rejection in
78
+ [popover-manual-poc](popover-manual-poc.md). It has been taken for tooltips,
79
+ which have no focus restoration, event or Escape entry to lose — see
80
+ [tooltip-coordinate-space](tooltip-coordinate-space.md).
81
+
82
+ Browser support is a precondition for either, never the trigger on its own.
30
83
 
31
- CSS Anchor Positioning does not cover 100% of our use cases. Context menus (repositionAt) open at dynamic pointer coordinates (clientX/clientY), not relative to a DOM element.
84
+ ## Review trigger
32
85
 
33
- Therefore, the JS engine cannot be entirely deleted. A lightweight JavaScript layer will still be required to capture the pointer event and pass the coordinates to the DOM (either by updating CSS variables --x/--y, or using the upcoming JS Virtual Anchor API).
86
+ Revisit when a script-observable anchor-visibility signal appears, or when a
87
+ project need makes hide-instead-of-close the wanted behavior.
@@ -47,6 +47,12 @@ and must match `[a-z][a-z0-9-]*`. Order is irrelevant; duplicates are harmless.
47
47
  <form data-enhance="validation autosubmit"> <!-- two behaviors -->
48
48
  ```
49
49
 
50
+ `applyEnhancement()` is an application-owned authoring convenience: it writes
51
+ the same tokens and refreshes their containing registrations. It does not watch
52
+ presentation classes or establish a second discovery contract. Walking the
53
+ scope's ancestors finds lifecycle owners even when the query scope is narrower
54
+ than the registration root, without crossing shadow boundaries.
55
+
50
56
  ## When a token, when a `data-*`
51
57
 
52
58
  > `data-enhance` is a generic opt-in for when HTML does not already provide an unambiguous one.
@@ -105,10 +111,10 @@ defeating the split.
105
111
 
106
112
  ## Design decisions recorded
107
113
 
108
- **D2 — `selectors.js` drops its 5 discovery entries and keeps 11 write-side
109
- state entries.** The file's job changes from *CSS↔init bridge* to *state
110
- vocabulary the runtime writes*. It stays the single file to alias for another
111
- CSS framework.
114
+ **D2 — `selectors.js` drops its 5 discovery entries and keeps 9 state
115
+ entries.** The file's job changes from *CSS↔init bridge* to *state vocabulary
116
+ the runtime writes*. It stays the single file to alias for another CSS
117
+ framework.
112
118
 
113
119
  **D4 — `status.js` uses `[data-status][role="status"]`, with no class and no
114
120
  token.** The markup already carries `role="status"` everywhere, so semantics
@@ -128,8 +134,10 @@ working.
128
134
  `menuFor()` validates semantically (`menu, [role="menu"]`). A context menu may
129
135
  legitimately want the flyout *look* without the flyout *behavior*.
130
136
 
131
- **D9 — Orphaned mounted surfaces are closed from the origin's disconnect
132
- handler.** A `fix:`, not part of the contract itself. Landed independently.
137
+ **D9 — An orphaned surface is torn down by a reaper observing
138
+ `data-actual-surface`.** `prepareSurface()` writes the marker; the reaper
139
+ closes and forgets a surface once it leaves the document. A `fix:`, not part
140
+ of the contract itself.
133
141
 
134
142
  ## Token vocabulary
135
143
 
@@ -407,7 +407,8 @@ meaning, rather than a stand-in for a coordinate system.
407
407
 
408
408
  ## Progressive enhancement
409
409
 
410
- Container queries are newer than Actual's Minimal browser baseline.
410
+ Container queries are guaranteed across Minimal, but not across Degraded — and
411
+ Degraded browsers still receive every stylesheet.
411
412
 
412
413
  Structural grids must therefore have a usable fallback without them.
413
414
 
@@ -780,6 +781,5 @@ A modifier introducing a second density ladder becomes defensible only when
780
781
  too conservative while wanting identical balanced-collapse behavior. One
781
782
  component in one miscalibrated container is not that evidence. A per-component
782
783
  threshold would also collide with the established meaning of `.compact` in
783
- Actual, which is padding density on a component (`.card.compact`,
784
- `.table.compact`) and never a layout threshold.
785
-
784
+ Actual: UI density, including tighter local padding on `.card.compact` and
785
+ `.table.compact`, never a layout threshold.
@@ -47,12 +47,17 @@ never about opting in.
47
47
  - **Base** (`utilities/base.css`) is a curated set of common ergonomic
48
48
  shortcuts (`.overflow-auto`, `.gap-none`, `.text-nowrap`, `.px`, `.py`).
49
49
  - **Extra** (`utilities/extra.css`) uses explicit property/value names
50
- (`.overflow-hidden`, `.gap-sm`, `.white-space-normal`).
50
+ (`.overflow-hidden`, `.padding-context`, `.white-space-normal`).
51
51
 
52
52
  Call them base and extra, after the files. "Optional" is the wrong word for
53
53
  either: it reads as a layer an author has to opt into, which is true of every
54
54
  module outside `core/` and therefore says nothing about these two.
55
55
 
56
+ The `.gap-sm` / `.gap-md` / `.gap-lg` scale was removed before 0.8: it set `gap`
57
+ without `--gap`, which can diverge from `.grid-N` track sizing. A local gap is
58
+ now a direct `gap` declaration; `--gap` is reserved for rebinding an inherited
59
+ rhythm.
60
+
56
61
  Base may use compact names for a small set of frequent operations (`.px`,
57
62
  `.py`, `.mbs`, `.mbe`). Extra uses explicit property/value names and must not
58
63
  duplicate an operation already provided by base. The two sets may cover the
@@ -157,7 +157,44 @@ Popover can take over the top layer without touching placement. Anchor
157
157
  positioning could later take over placement without touching lifecycle — though
158
158
  not yet, because the current positioner also reports *placement validity*, which
159
159
  the lifecycle uses to close a surface whose anchor has left the viewport, and
160
- declarative CSS positioning offers no equivalent signal.
160
+ declarative CSS positioning offers no equivalent signal. See
161
+ [css-anchor](css-anchor.md) for what else the positioner writes and what would
162
+ unblock the move.
163
+
164
+ ### Top-layer transport must not imply lifecycle ownership
165
+
166
+ A capability that solves one problem is not an invitation to hand it the
167
+ neighbouring ones. Popover bundles a transport — promotion to the top layer —
168
+ with a lifecycle, and the two are separable: `popover="manual"` supplies the
169
+ transport and nothing else.
170
+
171
+ That separation is what lets Actual use the native top layer while remaining
172
+ the owner of:
173
+
174
+ ```text
175
+ .is-open
176
+ Escape ordering
177
+ inside / outside dismissal
178
+ focus restoration
179
+ sheet policy
180
+ ```
181
+
182
+ The slide to guard against is "we are using Popover, so we should use its
183
+ lifecycle too". Those are two decisions, taken on different evidence. The
184
+ transport is judged on what it removes; the lifecycle is judged on whether the
185
+ component contract survives it — and for Flyout it does not, because
186
+ `data-flyout-auto-close` defaults to closing on an inside click, which native
187
+ light-dismiss cannot express.
188
+
189
+ The rule cuts the other way too, and that is where the boundary actually falls:
190
+ a capability is not obliged to take on a responsibility just because it looks
191
+ adjacent. A native `::backdrop` is painted for a manual popover and even
192
+ inherits custom properties from its originating element, but its UA
193
+ `pointer-events: none` is not overridable from author CSS — not with
194
+ `!important` either, since a UA `!important` outranks an author one. So it
195
+ cannot do the scrim's second job, absorbing the pointer, and the scrim stays an
196
+ Actual element. Adopting the transport did not make the platform responsible
197
+ for everything the old transport happened to carry.
161
198
 
162
199
  ### Ceding the properties a capability is expressed through
163
200
 
@@ -184,10 +221,13 @@ This is checkable rather than aspirational: any Actual class that carries a
184
221
 
185
222
  The corresponding trap is adopting a capability in a way that adds an axis
186
223
  instead of replacing one. A second surface transport living alongside the
187
- existing one would leave the DOM-reparenting code in place while doubling the
188
- behavior matrix, which is the opposite of the deletion this note asks for. The
189
- acceptance criterion for moving Actual’s own surfaces onto the top layer is
190
- therefore concrete: the change should remove more surface code than it adds.
224
+ existing one would have left the DOM-reparenting code in place while doubling
225
+ the behavior matrix, which is the opposite of the deletion this note asks for.
226
+ That is why the top-layer transport was adopted by raising the floor rather
227
+ than behind a capability branch: the axis was replaced, not added. Counting
228
+ lines was the weaker half of the criterion — the transport removed one concept
229
+ and a defect while the file grew by five lines. See
230
+ [popover-manual-poc](popover-manual-poc.md).
191
231
 
192
232
  ## Prefer capabilities over browser generations
193
233
 
@@ -237,7 +277,13 @@ Actual already has most of the visual building blocks through buttons, actions,
237
277
 
238
278
  A toolbar may be a useful small pattern, but its keyboard contract should preferably be built on a shared focus primitive rather than implemented independently.
239
279
 
240
- Do not add `.toolbar` merely as another horizontal flex utility.
280
+ Do not add `.toolbar` merely as another horizontal flex utility. If Toolbar is ever added, it is a semantic and keyboard enhancement over existing layout, with no CSS class of its own:
281
+
282
+ ```html
283
+ <div class="cluster" role="toolbar" aria-label="Formatting" data-enhance="toolbar">
284
+ ```
285
+
286
+ `.cluster` owns the layout, the controls keep their own presentation, `role="toolbar"` carries the semantics, and the enhancement supplies only the roving tab stop — which is `connectFocusGroup` almost exactly as it already exists. APG reserves the pattern for groups of at least three controls; below that the grouping earns nothing.
241
287
 
242
288
  ### Menu elements
243
289
 
@@ -483,7 +529,7 @@ If several widgets need the same roving-focus behavior, extract one reusable pri
483
529
 
484
530
  Consider a small Toolbar pattern only after the shared focus behavior is clear.
485
531
 
486
- Its value should come from semantics and interaction, not from duplicating `.cluster`.
532
+ Its value should come from semantics and interaction, not from duplicating `.cluster`. It is also not a reason to consolidate the focus primitive: `focus-group` is a documented public export with a real internal consumer in `menu`, so it does not need a second one to justify itself. Tabs in particular are not the missing consumer — they navigate *and* activate, sync `aria-selected`, toggle panels, and treat a valid panel as a candidacy requirement, so folding them in would trade little code for a subtle coupling between the focused item and the selected one. Wait for a third consumer to reveal the shared abstraction instead of designing it from two.
487
533
 
488
534
  ### Switch
489
535