One handle. Toggle "Snap to whole numbers" to compare a continuous value against one snapped to a `step` grid — then cycle the accent and container controls to see the fill hold up against every page accent and every real background context.
With no step, the value is an exact float, not snapped to a grid — the same control the "Ticks & format" subpage demos with a fixed step instead of this toggle.
<Configurator> accent picker is set to, live — because sciui's Slider reads the page --ac directly (its default is --sci-accent: var(--ac, …)). accentVars() sets only --ac on the wrapper, and the canvas picks it up with no per-Space override. The old cv question — whether the .cv-slider specificity override was still needed — is resolved: it was DELETED 2026-07-16, sciui reads --ac for everyone.<Configurator> (Phase 5 pilot) — accent + container switching is now free chrome instead of something each subpage would have to hand-build; the demo-specific control (the snap toggle) is the only bespoke part.A whole Beta distribution shaped by its 90% credible interval. Drag the interval's ends to set how sure you are (narrow = many draws, wide = few); slide the band to move the success rate. α and β are solved from the two bounds and stay continuous.
The interval's width is the number of draws: pinch the ends together and the curve pulls tall and narrow; pull them apart and it flattens toward the flat prior (α=β=1). All the Beta math runs in log-space Lanczos log-Γ, so α+β stays exact into the billions.
<Configurator>, so the "side gauges span the full panel height" requirement can be checked directly inside every real container (card/disclosure/tabs panel), not just the plain page — flagging for a browser check across all four, at a couple of the height slider's extremes.The whole contract, and nothing else. Usage is on the subpages that follow.
Selector
options SelectorOption[] { label, value, disabled? }
groups SelectorGroup[] { label?, options } — instead
of options; a run of chosen
options never spans two
value T[] THE CHOSEN SET — always a set
onChange (value: T[]) => void always a set, for the same
reason
max number = 1 how many may be chosen at
once; choosing past it evicts
the OLDEST. Infinity = no cap
min number = max === 1 ? 1 : 0 how few may remain
layout 'row' | 'column' = 'row'
collapse false | 'overlay' | 'inline' = false
shown whole, or behind a
trigger: over the page, or
in place
density 'default' | 'compact' = 'default'
size 'xs'|'sm'|'md'|'lg'|'xl' = 'md'
scroll boolean = false overflow along the main axis
instead of squeezing
frost boolean = false see-through backdrop; stable
backdrops only
shadow 'inset' | 'outset' | 'none' = 'inset'
bare boolean = false no shell surface at all; with
collapse="inline" it strips
the PANEL only
push boolean = true inline only: open pushes what
follows down, or overlays it
open boolean? controlled; omit for internal
onOpenChange (open: boolean) => void
label ReactNode?
labelPlacement 'outside' | 'inside' = 'outside'
placeholder ReactNode? trigger text for an empty set
summary ReactNode? override the derived summary
className string?
style CSSProperties?
aria-label string?
THE FOUR SHAPES, out of two numbers
max=1 min=1 a switcher — exactly one, always (the sliding pill)
max=1 min=0 a toggle — one or nothing
max=∞ min=0 a checklist — any number, none included
max=3 min=1 up to three, never fewer than one
CSS
--sel-q length the quantum = --sciui-q
--sel-thick length cross-axis size = 9 * --sel-q
--sel-r length corner radius = --sel-thick / 2
--sel-pad length shell inset, and the pill's gap= --sel-q
--sel-pill-r length the pill's own radius = --sel-r - --sel-pad
--sel-motion duration the pill's glide = 190ms
--sel-fill color the shell's surface = --sel-surface
--sel-pill-bg color = --sel-accent
--sel-rim-w length rim width; :focus-visible doubles it
--sel-rim-color color = ink 8%
--sel-inset-color color inner band = transparent
--sel-cast-color color outset cast = --sciui-cast
--sel-pop-surface color the floating list, OPAQUE = ink 6% over base
LAWS
every surface is an <Sq> — track, pill, trigger, popup, inline panel
a PILL IS A RUN, not an option: adjacent chosen options are ONE surface
(<Sq.Slide follow={[first, last]}>), so there is no seam to hide
the pill is keyed by POSITION, so a change of choice glides
the chosen label changes COLOUR only — never weight: switching must not
reflow the text. The pill is the signal
where the engine has anchor positioning, the floating list is a popover in
the top layer, tethered by CSS — nothing is measured and no scroll is
listened to. Elsewhere it falls back to a measured portal
NOT COVERED YET
RTL — the row mixes physical direction with logical padding
the concentric radius channel does not reach a nested <Sq> (a --sq-radius /
--sq-r cycle), so the pill states --sel-pill-r rather than inheriting it
A generic float popover for when the panel needs to hold MORE than one option list — several independent controls, a small form — not just a fixed set of values. Same closed-shell trigger and portaled, squircle-clipped panel as Selector's own dropdown, generalised over its content instead of its options.
The panel is portaled to document.body (never clipped, never buried by a stacking context) and measured on open, re-measured on scroll/resize — the same mechanics as Selector's float popup. Unlike Selector, the panel does NOT close on Tab and does not rove focus over its own children: the content is arbitrary and may itself hold several independently-tabbable controls, so only the trigger, an outside click, or Escape close it.
/push pair exists for the push-in-place or overlay-without-portal cases; nothing here re-implements that for arbitrary content yet.min(92vw, 26rem) — a consumer with a genuinely wider panel can override via the panelStyle prop.The prev/next buttons live in the container's own flex track, never in the scroll flow, so they never move when the active slide changes or the component reflows. controlsPosition only changes where that track sits: above the scroller, below it, or split to the scroller's sides.
controls="top" (default)
controls="bottom"
controls="split"
carousel.tsx's own header comment, 'split' is a deliberate, documented exception to the "controls never move" rule: on mobile widths its buttons detach from the flex track and travel with the page scroll, parked at mid-viewport (see the media block in carousel.css). That's an intentional design, not a regression — but flagging it here so the exception reads as obviously intentional rather than like a broken anchor, since at desktop width this page can't tell the two apart (the split buttons look statically pinned like the other two modes until the viewport narrows).<Configurator> (Phase 5 step 6): this subpage's whole point is a fixed three-way side-by-side comparison ofcontrolsPosition values, not one configurable instance — collapsing it into a single picker would hide exactly the simultaneous "top vs bottom vs split" contrast a reader needs to see. Flagging this call explicitly rather than forcing it.The whole contract, and nothing else. Usage is on the subpages that follow.
Tabs
labels ReactNode[] one per panel, in order; ANY
node — badges, counts, icons
children ReactNode each direct child is one panel
index number? controlled active index
defaultIndex number = 0 uncontrolled initial
onIndexChange (index: number) => void
side 'top' | 'right' | 'bottom' | 'left' = 'top'
strip edge AND the weld side
size 'xs' | 'sm' | 'md' | 'lg' | 'xl' = 'md'
lines number = 1 vertical only; 1 = no wrap at
all, strip = longest label.
N = narrowest width at which
no label exceeds N lines
shadow 'inset' | 'outset' | 'none' = 'inset'
cast 'near' | 'far' = 'near'
rim number = 0 ring width in px, 0 = off
disabled number[]? tab indices; unreachable,
still occupy their place
mount 'active' | 'all' = 'active' 'all' keeps every panel in the
document, hidden — for SEO,
in-page find, kept state
start ReactNode? any content at the strip's
end ReactNode? two ends; not tabs, not in
the tab order
className string?
style CSSProperties?
aria-label string? lands on the tablist
…rest forwarded to the root element
CSS
--tabs-r length corner + ear + panel radius = --sciui-r ?? --r
--tabs-q length spacing quantum = --g
--tabs-strip-h length tab depth = 2 * --tabs-r
--tabs-strip-t length strip thickness = 2R | measured
--tabs-pad length end clearance = 2 * --tabs-r
--tabs-alpha number panel tint = 0.06
--tabs-under color what the panel is mixed over = --sq-under
--tabs-ink color = --ink
--tabs-accent color = --ac
--tabs-rim-color color the ring on the union = ink 22%
--tabs-cast-color color outset cast (never the ink) = --sciui-cast
--tabs-motion duration weld glide = 220ms
LAWS
the panel is one <Sq>; the active tab is a guest welded onto it
2R clearance — every tab keeps 2R from a panel corner (structural: equal
tabs + 2R strip padding)
the tab is 2R deep on every side; only the strip's thickness turns
keyboard follows the axis — ←→ horizontal, ↑↓ vertical
the panel is OPAQUE — an outset cast lands behind its silhouette
a cast never follows the ink; the inner band always does
NOT COVERED YET
RTL — the strip mixes physical row/row-reverse with logical padding
labels[] and children[] are paired BY POSITION; a length mismatch is
tolerated silently (extra labels get an empty panel)
Used on its own as a plain settings toggle — deliberately paired with two labels of very different length ("Off" vs "Enabled") so the no-layout-shift behaviour is visible: the button's width is fixed by the wider label from the start, since both labels are always in the DOM (one just invisible).
Toggling the switch flips between "Off" and "Enabled" — watch the button's own width as the label changes: it should not resize or reflow anything around it.
A single expand/collapse panel — click the header to reveal its content. Unlike an accordion, one <Disclosure> is exactly one region; a consumer wanting several independent panels composes multiple instances (there is no built-in exclusive-open group behaviour).
This is the panel content. It's revealed by a pure-CSS height transition (grid-template-rows: 0fr → 1fr) — no JavaScript measures the content's height, so there's nothing to keep in sync when the content itself changes shape (e.g. on a language switch elsewhere in the app).
Opening this one has no effect on the panel above — each instance owns its own state.
nested prop.The whole contract, and nothing else. Usage is on the subpage that follows.
Input
value string? controlled
defaultValue string? uncontrolled start
onValueChange (value: string) => void every keystroke, as the VALUE
onSubmit (value: string) => void Enter, trimmed. Shift+Enter is
the newline, so it never submits
placeholder string?
multiline boolean = true false = one line, never wraps
rows number = 1 lines tall at rest — the floor
maxRows number = 6 lines it grows to before the
text scrolls inside it.
Infinity = never stops growing
before ReactNode? inside the surface, beside the
after ReactNode? text — a glyph, a unit, a send
button. Centred, one gap away
disabled boolean? not editable, reads unavailable
readOnly boolean? not editable, reads ordinary
autoFocus boolean?
rim number | string = 1 the rim's width AT REST; focus
thickens it to the focus width
shadow 'none' | 'inset' | 'outset' = 'none'
size 'xs'|'sm'|'md'|'lg'|'xl' = 'md'
name · id · maxLength · spellCheck · autoComplete · inputMode
onKeyDown · onFocus · onBlur onKeyDown runs FIRST — call
preventDefault() to own Enter
className · style · aria-label · aria-labelledby · aria-describedby
aria-invalid
ref the editable element itself (<textarea> or <input>)
CSS
--in-q length the quantum = --sciui-q
--in-thick length the box's minimum cross size = 9 * --in-q
--in-r length corner radius = --in-thick / 2
--in-pad length the text's inset, all four sides= 1.75 * --in-q
--in-gap length text ↔ before/after = 1.5 * --in-q
--in-font length = 2.5 * --in-q
--in-line number line height = 1.4
--in-min-h length from rows (state it to override the line math)
--in-max-h length from maxRows ( " )
--in-fill color the surface = --sciui-wash
--in-rim-w length composed; focus swaps it = --in-rim-rest
--in-rim-color color = ink 8%
--in-ink color = --ink
--in-accent color = --ac
LAWS
the box is ONE <Sq> — shape, fill and rim are the primitive's, not this
component's
FOCUS IS THE RIM, THICKER — never a second ring. A ring outside the rim is
two edges on a control that has one, and only the inner one is a true
Lamé curve
it GROWS, it never shows a scrollbar (owner): the height is written from the
content's own scrollHeight between the two line counts, and past the
ceiling the text scrolls with no bar drawn, in every engine
the geometry is the library's, not this control's — 9q tall, R = 4.5q,
1.75q text inset: an Input beside a Selector or a Dialog composer is the
same object
a shadow, if asked for, is the APPLICATION's — the ink comes from
--sq-inset-color / --sq-cast-color (f-ish theme.css) and the recipe from
the primitive, so no component owns a depth of its own
NOT COVERED YET
a leading/trailing label INSIDE the rim that is not a slot (a currency prefix
that the caret must not enter) — pass it as "before" and it is a slot
validation state beyond aria-invalid: no rim tone of its own yet
A bottom-anchored chat surface: a scrolling history, a status plate of live metrics, and a composer whose input and send button are visually identical to a Selector track and pill.
Content rendering is external: every child is opaque to the Dialog. DialogMessage is the default text bubble. The status plate takes a segments array — each entry is either a template dict (title, value, optional max → bar, optional message → caption) or any custom node. The plate above shows every combination side by side, ending with a fully custom segment. Turn History off to see the landing state — just the composer, centred — then send a message and watch it glide to the bottom.
Wrap any single element — <Tooltip content="…">{child}</Tooltip> — and it shows on hover or keyboard focus, after a short delay so it never flashes on a quick mouse pass-through. side is a static choice, not an auto-flipping engine. The panel is measured and portaledonce per showing — never re-synced to scroll — so it can escape a clipping container without ever "moving" under JavaScript; see the Anchoring subpage for what that buys and what it costs.
The four sides share one geometry rule: the merged corner always sits dead-centre on the trigger's own edge, exactly touching it — never a gap, never an overlap. That's measured, not guessed: the arrow's own rendered size (a square rotated 45deg) gives its exact centre-to-tip distance, and the panel is placed precisely that far from the trigger, once, when it opens. Only opacity transitions on entry/exit — never transform/filter, the one motion law every sciui/f-ish surface holds (the arrow's rotate(45deg) is static, set once, never animated).
useSquircleClip) should get the same real-Firefox check every other sciui corner does; Chrome's native corner-shape path is not a reliable proxy for it.<span> (display: inline-block), not a cloned child — simplest, works with anything, but it does add one extra box to the DOM, which is occasionally visible in a strict flex/grid layout that cares about direct children.side is a static author choice, and a badly-chosen one near a viewport edge stays exactly wherever the author put it. There IS a portal (to the nearest .fish ancestor, not document.body) specifically so a tooltip inside a clipping container (a Card, a scrolled panel) is never cut off — see the Anchoring subpage.The three standalone heading elements, then FHeading — the canonical icon/title/kicker block used to build a page's rich rail menu item.
FHeading combines an optional icon, a title and a kicker into one canonical block:
A Space never places this itself any more — export subtitle from a page's first subpage and the site loader (create-site.ts) builds this exact block for the rail's menu item automatically: title = the page's resolved kicker label, kicker = the subpage's subtitle, icon = the subpage's icon export. No placement flag — subtitle's mere presence is the trigger (the "subtitle-set" menu mode). This very Page does exactly that (see this subpage's own subtitle export) — check the rail: "Typography" now reads as a two-line block, not the plain pill every other Page here uses.
heading/menu={false} export + placement-flag mechanism this subpage used to demo is GONE (2026-07-15 header/menu rework) — replaced by the inferred kicker/subtitle/icon resolution described above, and this Page's own menu item now demonstrates the subtitle-set case live (see Squircle for the icon slot, Cards & Stats for a fully custom menu export — between the three, every menu-item mode this template supports has a real, end-to-end example somewhere on this Space).<Configurator> (Phase 5 step 6): these are structural markup elements (kicker/title/subhead/heading), not components with tunable props — there's no real "knob" to expose beyond the text content itself, and this Page's own menu item is a live subtitle-set example that shouldn't be disturbed by wrapping its subpage content in unrelated demo chrome. Flagging this call explicitly rather than forcing it.FButton — a soft pill: accent fill, crust-dark label. Like every kit element it takes an optional size step (xs·sm·md·lg·xl) via data-fsize.
Every step is the same component and the same markup — only the size prop changes, which rescales the button (and its label) proportionally via the shared --gt/--g scaling unit.
FButton has no dedicated variant prop in content.tsx — only the generic size (from the shared Sized type) plus whatever className/native button props are passed through. So the "couple of use cases" the spec asks for are shown here purely as size steps and differing labels/context, not as invented variant props.FCard is a surface-toned, soft-cornered block — plain, or in a media chip layout, or broken out to full panel width with wide. The two are independent boolean modifiers, so toggle them together to see whether the media chip layout and the full-width breakout actually compose.
A default FCard with an FCardBody inside — the baseline shape, no modifiers.
content.tsx documents media and wide as independent boolean props applied as separate class modifiers (fish-card--media and fish-wide), with nothing in the source suggesting they're mutually exclusive — so combining both should just stack the classes. Now on the shared <Configurator> with both as independent toggles, so "media + wide together" is directly reachable (it wasn't in the old three-fixed-card layout) — worth a live check that the full-width breakout and the fixed-width media chip actually compose cleanly rather than assuming it from the source alone.menu export (a hand-rolled metric badge) is this Space's only live demo of fully custom menu content — see this file's own comment above it. "3" counts the card variants the two boolean toggles can reach (plain/media/wide, not counting the media+wide combination as a distinct "style") — a hand-typed number that would silently go stale if a variant is ever added or removed here.A full-width, fluid, self-balancing grid of <Card>s. Children with no expand stay a zero-cost static grid; any child carrying expand turns the whole grid into a hover/tap/focus-activated reveal — the exact mechanism cv's Home focus tiles and Portfolio hub run on.
No expand on any card — no state, no listeners, no measurement.
A card is a shape: an optional title band, a body slot, up to four corner decorators.
An href card gets the built-in "go" chevron bottom-right FOR FREE — no decorator prop, no <CardDecorator> child, tinted by this card's own accent automatically, drawn via the built-in <Icon id="ph:caret-right-fill" />.
Each card's own accent sets --ac on its root — everything inside inherits it. The expand block below lives OUTSIDE the card in the DOM, so the grid forwards the same card's accent onto its own reveal set. The THIRD card sets no accent at all — open it and its chips take the page's own ambient accent instead, proving the fallback holds.
maxColsLong, multi-paragraph card bodies read too narrow once a wide screen fits 4 columns — the fix is a column CAP the author names directly, not a min-width to guess at. maxCols={2} below never lets more than two columns form, at any viewport width, so these long bodies keep a readable measure instead of squeezing into a fourth of the row.
A durable data platform earns trust in stages: first by being correct, then by being fast, and only then by being convenient. Skipping ahead to convenience — nice dashboards over ungoverned, unvalidated pipelines — is the most common way teams end up re-litigating "which number is right" in every planning meeting.
The unglamorous work is schema contracts, tested transformations, and a single source of truth for each metric's definition, published somewhere every stakeholder can actually find it before they build their own spreadsheet version.
Running more A/B tests is not the same as learning faster. A team that ships ten underpowered tests a quarter and calls every marginal p-value a win is optimizing for the appearance of rigor, not for decisions that would have gone differently without the data.
The higher-leverage habit is pre-registering what would change your mind, sizing the test to actually detect that effect, and being willing to report a clean null result as a genuine, useful outcome rather than a failure to be quietly shelved.
The fastest ramp isn't a wiki page, it's a first real ticket paired with someone who already knows where the landmines are — which tables are deprecated but still queried by legacy dashboards, which metric names mean two different things in two different teams, which joins silently fan out rows.
Write that landmine list down as you go, even informally; the second new hire should never have to rediscover the same trap the first one just fell into.
maxCols is pure CSS — it sets --card-grid-maxcols inline, no JS layout logic, so the mobile single-column mode below the container breakpoint is completely untouched; the cap only ever governs the WIDE-mode column count.
A <CardDecorator corner="tl|tr|bl|br" icon={…} background={…} /> is a slot CHILD — declared inside a card's own children but never itself rendered; the card reads it off the tree and positions it, radius concentric with its own corner in every case, symmetric at all four. The icon is sized as a FRACTION of the decorator's own box (60%, bold on purpose — never a fixed em/px), and each one below is sciui's own <Icon id="ph:caret-right-fill" /> — a Phosphor glyph resolved against the generated registry (see NOTES.md § Icon system); it paints currentColor, so it inherits the decorator's own accent automatically. An href card shows the built-in "go" chevron bottom-right automatically (see the Pipelines card above) — an explicit decorator prop or a corner="br" child, like below, overrides it; none of these four cards has an href, so nothing here is a default, every decorator below was asked for.
Inset one gap from the card's own top-left corner.
Top-right.
Bottom-left.
Bottom-right — where the built-in nav default and the legacy decorator prop both land.
bare renders NO squircle plate at all — icon only, same position, same size box, just no fill at rest or on hover (cv's Home tiles use this for their six meaning-icons). hoverFx picks the hover motion, author-selectable, default 'grow': 'grow' transitions the ICON's own width/height inside its fixed, centred box; 'shift-right' is a ± margin pair sliding the icon rightward (the box itself never resizes); 'none' is static. Both motion variants are compositor-legal — never transform/filter — gated to real pointer hover with prefers-reduced-motion: no-preference. Hover each card below to feel the difference (mouse only — try each on a real pointer device).
No plate — just the icon, tinted by background even though nothing is painted.
The icon slides rightward on hover, margin-only — cv's Portfolio uses this for its linked cards.
The built-in <Icon id="ph:lock-fill" />, static — cv's locked Portfolio cards use exactly this.
A plain <ul>/<li> in a card body needs no classes — compact spacing, a marker in the same muted tone as the body text.
<p> gets, so it aligns identically.disabled renders a plain, non-interactive element regardless of href/onClick — no hover lift, no cursor affordance, and no automatic nav chevron either. cv's Portfolio uses this for demos that exist but aren't public, wearing a static <Icon id="ph:lock-fill" /> decorator instead (see "No motion" above).
This card fronts nothing reachable — it never lifts, never gets a pointer cursor.
corner-shape path can't stand in as a proxy for the clip-path polyfill.href and disabled (cv's Portfolio never combines them) should show no chevron at all — Card's default-nav check is gated on !disabled, but worth a live confirmation since it's an easy corner to miss.<Configurator> — this page is several distinct fixed illustrations (plain / expand / decorators / disabled), not one configurable instance, the same call the Chips & Stats page made for its own multi-accent chip row.A self-contained segmented switcher — pass options, a controlled value, and onValueChange; the sliding pill and its settle-recoil come for free.
Under the hood FSegmented mounts its own Tabs.Root and hands the bare SegmentedList the activeValue it needs for the recoil — see the "Bare list & shadows" subpage for the same visual composed manually.
options/value/onValueChange is a small, ordinary controlled-component surface, and the pill's settle-recoil direction is derived internally from the value change, so there's nothing for a consumer to get wrong here.The whole contract of <Sq>, <Sq.Dock> and <Sq.Slide> — and, below it, every mode each prop names, shown rather than described. Usage prose is on the subpages that follow; all the knobs at once on one surface are the constructor at the end.
Sq — the surface primitive
as ElementType = 'div' the element rendered. A crop
wrapper follows a phrasing
host (span/button/a/label/
summary/legend) with a span
radius step | number | CSS length the scale is none xs sm md lg
xl = 0, .25, .5, 1, 1.5, 2 of
the AMBIENT --sq-radius, which
a parent surface republishes
to its children. md IS that
radius. Omit to inherit it
squircle number = 1.5 the exponent; the Lame exponent
is 2 ** squircle. Each distinct
value costs ONE extra rule set
for the whole document
pad number | CSS = R / 2 the inner inset — and the
concentric law: pad + a child's
radius = this radius, at any
depth
shadow 'none'|'inset'|'outset' = 'none'
ONE shadow. The two are
mutually exclusive, and that is
the whole shadow model
cast 'near' | 'far' = 'near' how far whichever was chosen
reaches: near rests on the
page, far floats above it
rim number | CSS = 0 the hairline's WIDTH, lying ON
the curve, inside it. The width
IS the switch — there is no
separate boolean. Interrupted
along a welded side, where a
line across the joint would
betray the seam
bg CSS colour? the fill. Unset paints none
fg CSS colour? content colour, set on the host
rimColor CSS colour? unset paints no rim even with a
width
shadowColor CSS colour? THE SHADOW'S INK, never its
finished tone — each recipe
takes its own fraction of it.
Unset takes the ambient
--sq-inset-color / --sq-cast-
color, which is how a whole
application gets ONE inner
shadow and ONE cast with no
component naming a colour.
NOTHING is ever derived from
currentColor: with no ink,
nothing is painted
weld side | side[]? which of THIS surface's sides
are welded to a neighbour. A
welded side COLLAPSES BOTH ITS
CORNERS TO SQUARE: the radius
is multiplied by the corner's
own factor (1 - d_h)(1 - d_v),
so one welded side squares the
two corners it owns and a
second welded side squares its
own. THERE ARE NO EARS HERE —
a concave ear is the geometry
of a GUEST on a host (Sq.Dock),
and nothing else. The rim and
the inset band fade to a band
of zero width along a welded
side, by the same (1 - d)
factor, and a welded surface
draws NO CAST at all — a row's
cast belongs to whatever
contains the row.
The opposite direction of
Sq.Dock, which is a guest
welded onto THIS surface —
which is why the two no longer
share the word "dock"
crop boolean = false wrap the children in ONE
service box and cut them to the
exact silhouette, a docked
guest's bump included
motion number(ms) | CSS = 0 the joint's clock: how long a
welded guest takes to follow
when its numbers change. 0
jumps. A guest may override it
type 'button'|'submit'|'reset'? for as="button"
className · style · ref · children · …rest forwarded to the host element
CROP — the contract, in full
The one observable difference from a plain div: the children become
GRANDCHILDREN of the host. So no structural selector may cross the host
boundary — .my-host > .child, :scope > *, firstElementChild — and the children
have to be styled by their own classes instead. Everything else behaves exactly
as a div does: an absolutely positioned child still resolves against the HOST
(the wrapper is unpositioned), percentages, sticky and scrolling are unchanged.
Off by default not out of caution: a surface paints BEHIND its content, so a
child reaching past the curve is the ordinary state — focus rings, overlays,
corner decorators, and every docked guest, which hangs outside by construction.
What must be cut is what SCROLLS. The cut cannot be taken on the host, which is
why asking for it adds an element rather than a declaration: the host is also
where the paint hangs, and no CSS tells paint from content inside one subtree.
Measured, on the polygon path: a clip on the host cost an outset surface its
whole cast and a docked one its guest.
Sq.Dock — one guest welded ONTO a surface
side 'top'|'right'|'bottom'|'left' required. Every guest on one
surface names the SAME side
at number | CSS = 0 offset ALONG that side from its
NEAR end — left for a
horizontal side, top for a
vertical one, so a guest does
not reverse when it moves from
the top edge to the bottom.
Ignored when follow is set
size number | CSS | 'auto' extent along the side. Never
less than 2R — that is the
narrowest bump the corner rule
can express: two ears meeting,
no crown between them. 'auto'
takes that one axis back off
the generated rule, so the
guest is an ordinary
shrink-to-fit box and the
measured number travels ONE WAY
to the patch. The bump SNAPS to
each new content width rather
than gliding to it: a guest
that grew its own label is
layout, and nobody asked for
layout to be animated
depth number | CSS how far the guest stands OUT of
the host. 0 means the guest is
ABSENT — the ear radius is
min(R, depth / 2) and collapses
with it — which is how a guest
animates in and out rather than
appearing at full height
follow element | ref | null take at and size from that
element's box instead of
stating two numbers. The
measurement joins the ONE batch
this layer already runs: no
observer of its own, no rect
read per placement
motion number(ms) | CSS this guest's own clock,
overriding the host's
as · className · style · ref · children · …rest
Sq.Slide — a surface that slides INSIDE another (every Sq prop, and)
axis 'x' | 'y' = 'x' which way it travels; the cross
axis is stretched to the host's
padding box
follow el | ref | [from, to] | null the box — or the two ends of a
RUN of boxes — it takes its
offset and extent from. A RANGE
spans the union of the two, so
adjacent chosen options read as
ONE pill: not several surfaces
with their shared corners
squared off, which can never
meet exactly, but one surface
with no shared edge to miss.
null renders nothing, which is
how a selection of nothing is
drawn
gap number | CSS = 0 the inset it keeps from the
host on the CROSS axis, both
sides. The host's own padding
cannot stand in for it:
absolute offsets resolve
against the padding box, which
is inside the border and
OUTSIDE the padding
CSS — every knob is also a custom property, and five have no prop at all
--sq-radius length what a parent publishes to its children
--sq-r length this surface's radius = --sq-radius
--sq-pad-in length the inner inset = --sq-r / 2
--sq-exp number the exponent = 1.5
--sq-bg colour the fill
--sq-rim-w length rim width
--sq-rim-color colour
--sq-inset-color colour the INK of an inset shadow
--sq-cast-color colour the INK of an outset cast
--sq-motion duration the joint's clock = 0s
--sq-motion-ease easing = cubic-bezier(.2, .7, .1, 1)
--sq-slide-gap length Sq.Slide's cross-axis inset = 0px
--sq-inset-strength percent the inner band's tone = 5%
--sq-inset-depth number its blur, as a multiplier = 1
--sq-shadow-strength number the cast's tone, as a multiplier = 1
--sq-dpr / --sq-u the device grid — published BY the layer
those last three are THEME level and deliberately do not become props: they are
what an application tunes once, not what a call site states
.sq-row / .sq-col the gap law as a utility, clamping the gap to
--sq-r1 + --sq-r2 with nothing imported
[data-sq-fallback] force the polygon path in an engine that has
the native one — so the fallback is compared
without a second browser
LAWS
ONE SHADOW per surface; inset and outset are mutually exclusive
NO COLOUR IS EVER DERIVED — an unset colour paints nothing rather than
inventing a tone out of currentColor
a guest is a CONTENT BOX, never a second surface: the host paints the entire
union silhouette, so an <Sq> inside a dock draws a second outline inside the
first and doubles the line at every ear
a dock is a DIRECT CHILD of its <Sq> — the host reads these props to build the
contour, and the contour names every guest before a stylesheet for it can be
chosen. A wrapper around the dock breaks the relation
every guest on one surface names the SAME side
size >= 2R, and 2R of clearance from a host corner AND from the next guest.
There is no clamp in CSS and that is a decision, not an omission: the clip is
written against a box two blur margins wider than the host, so a clamp would
have to hang off each of ~90 occurrences of at. The CALLER holds the law;
follow holds it by construction, because it takes real layout numbers
the host needs a straight run of 2 * (R + 3 sigma) on the perpendicular axis —
shorter than that there is nothing for the patch to hand back to
bg MAY BE TRANSLUCENT WHERE A GUEST IS DOCKED: the host's fill stops at the cut
and the patch draws on from there, so the two abut instead of overlapping and
the seam never shows. WELDED NEIGHBOURS ARE THE OPPOSITE CASE — they must
share one OPAQUE fill. There is no skirt to hide the seam any more: it is a
plain butt between two boxes layout puts flush, and two antialiased edges on
one line multiply their coverage instead of sharing it. What keeps it clean
is the grid, not an overhang — both sides derive the boundary from one real
number through the same rounding
two surfaces either WELD or stand R1 + R2 apart — between those states the
facing curves pinch into an unreadable channel
the native branch is taken where corner-shape exists, the surface carries no
guest, and — since a squared corner IS natively expressible — a WELDED one
goes native too. The two exceptions are weld + rim and weld + shadow, which
stay on the polygon because box-shadow is not per-side and would draw the
ring or the band straight across the seam. The branch is @supports, never
JS, so prerendered markup is engine-independent and hydration cannot
desynchronize
nothing measures anything per frame: the contour is one clip-path whose
coordinates are calc() over the surface's own properties, so a resize, a
fluid radius, a width transition and a dock switch all re-resolve inside the
CSS engine
NOT COVERED YET
Safari — never checked, in any mode
a NESTED <Sq> does not inherit the concentric radius: --sq-radius is produced
from --sq-r and consumed as --sq-r, so on a child that redeclares both the
chain is a cycle and computes to nothing. A nested surface states its radius
a welded surface's cast is still a drop-shadow on the skin, with a known defect
over a translucent page background
RTL — at is physical (from the left, or the top), not logical
radius — the six-step scale, and any length beside it. Every step is a multiplier of the ambient --sq-radius, so one knob rescales a whole screen. There is no pill step: a capsule is not a mode, it is what the geometry produces when a surface's smaller side reaches twice its radius.
squircle — the exponent. The Lamé exponent is 2 ** squircle: 1 is a plain circular arc, 1.5 the default superellipse, higher is boxier. Each distinct value on a page costs one extra rule set for the whole document, which is the reason it is a prop and not a per-instance number.
pad, and the concentric law. The default is half the radius, and that is not a taste: a child surface inherits parent R − pad as its own ambient radius, so pad + child R = parent R and the two curves stay concentric at any depth. State a pad and the children re-derive from it.
shadow and cast — one field, four pictures. Inset and outset are mutually exclusive, because a surface has one shadow; cast scales whichever was chosen. The inset is a band of width σ centred ON the curve and cut by the fill; the outset is the silhouette blurred and cut by an evenodd ring OUTSIDE it, which is why a translucent fill is not tinted by its own shadow.
rim — a width, not a boolean. Zero is off; there is no second prop to forget. The hairline lies ON the curve rather than outside it, so it is a true Lamé ring and not a rounded rectangle approximating one — and rimColor is separate, because a width with no ink paints nothing.
The four colours, and the one rule they share. bg, fg, rimColor and shadowColor derive nothing: unset means the effect is not painted at all, never that a tone is invented out of currentColor. shadowColor is the shadow's INK and not its finished tone — each recipe takes its own fraction of it, so the same ink reads as a 5% inner band and a 30% cast.
crop — asked for, and rarely. A surface paints behind its content, so a child over the edge is the ordinary state; what must be cut is what scrolls. The cut is taken on a content box of the surface's own (.sq-crop), never on the host — the host is where the paint hangs, and a clip there took the cast, the joint's bump and every docked guest with it. The whole price is that the children become grandchildren, so a structural selector may not cross the host boundary: style them by their own classes.
weld — which of THIS surface's sides are welded to a neighbour. A welded side COLLAPSES BOTH OF ITS CORNERS TO SQUARE, and that is the whole corner rule: each corner carries a factor (1 − d_h)(1 − d_v) that multiplies its radius, so a side going welded takes the two corners it owns to zero and leaves the other two alone. There are no ears here — a concave ear is the geometry of a guest standing on a host, which is Sq.Dock below, and it never belonged to the corner rule at all. Along a welded side the rim and the inset band fade to a band of zero width by the same factor, and a welded surface draws no cast whatsoever: a row of welded panels casts as a row, which is its container's business, not each panel's.
And the seam is a butt joint now, not an overlap. A welded surface used to carry its fill past the welded edge on a skirt, so the join lay inside one polygon and never had to be rasterized as a boundary. That is gone, along with the layer inflation that held it — nothing this primitive draws leaves the host's border box any more. The price is that welded neighbours must share one opaque fill and land on the device grid: two antialiased edges on the same line multiply their coverage rather than sharing it, so a boundary off the grid shows the page through it. Both sides derive that boundary from one real number through the same rounding, which is what makes the pair exact.
The two directions of one relation — and the ears are the whole difference. On the left the small surface declares weld="bottom": it is its own <Sq>, with its own fill and its own contour, its bottom two corners squared, and the two shapes meet along a flush edge. No ears. On the right the panel declares <Sq.Dock side="top">: there is ONE contour, the guest is a bare content box, and where the bump rises out of the panel the host grows a concave EAR on each side of it. Ears, both of them.
That is the rule to carry away: an ear exists only where a guest stands on a host, because an ear is what a silhouette does when it has to swallow something. Two panels laid against each other are not swallowing anything — they are a flat join, and a flat join wants square corners. Use weld when the neighbour is a surface in its own right; use Sq.Dock for a bump on somebody else's panel — an <Sq> inside a dock draws a second outline inside the first and doubles the line at every ear.
Sq.Dock — side, and the two axes that are not width and height. A guest on the left side has no width along the side, so the props are size (along) and depth (out), side-agnostic like the rest of the layer. at is measured from the NEAR end of the side — left for a horizontal one, top for a vertical one — so a guest does not reverse when it moves from the top edge to the bottom.
Sq.Slide — a surface that slides INSIDE another. The opposite of a dock in every way that matters: it does not stand out of its host, so the host's contour never learns about it, so there is no patch, no notch and no generated stylesheet — and where corner-shape is native it costs no polygon at all. A range spans the union of two boxes, which is how a run of adjacent chosen options reads as ONE pill rather than several surfaces whose shared edges can never meet exactly.
Pick the page's own accent — including "None," the template's real accent-less mode — then a container and an inner element (each independently accented) to check any nesting/coloring combination the template can produce. "Nested container" as the element reveals a second, inner container + element pair — exactly two levels, no further nesting.
Every container is a real, reusable primitive (FCard, sciui Disclosure, sciui Tabs) — none of this composition logic is a new component; it only decides which existing one to render and how to accent it. The page accent above is the real thing (useFPageAccent()), so "None (plain page)" as the container reads exactly as it would on any real page of that accent.
--ac accent on their own (they read their own --sci-accent) — this lab's CONTAINER/ELEMENT accent pickers set both together per subtree (`accentVars()`), but the page-accent change above only sets the real `--ac` (matching what a real Page does) and does not also drive `--sci-accent` — so any bare sciui control on this page outside `ContainerLab` (e.g. these picker rows themselves) won't visibly track the page accent. Consistent with how every other Space's sciui usage already works, but worth flagging since this page makes accent-tracking gaps very visible by design.Kit prose owns no block margins; the CONTAINER spaces its children by one step (--flow), with a few tight exceptions (kicker→title, title→lead, text→note). The same mixed document below reads with identical rhythm in every container — the reading column, a card, a disclosure, a tabs panel — because the container, not the element, owns it.
.fish-flow wrapper needed; every content container is a rhythm owner.A short standfirst, sized as a lead paragraph — the intro line under the title.
First body paragraph. Prose primitives (FText) and sciui components interleave below so the vertical rhythm between unlike neighbours is directly visible.
A second paragraph, so paragraph-to-paragraph spacing is on show too.
Body copy sitting between two sciui controls — each gets the same gap a paragraph would.
The identical document, dropped into each surface — the rhythm does not change with the container.
A short standfirst, sized as a lead paragraph — the intro line under the title.
First body paragraph. Prose primitives (FText) and sciui components interleave below so the vertical rhythm between unlike neighbours is directly visible.
A second paragraph, so paragraph-to-paragraph spacing is on show too.
Body copy sitting between two sciui controls — each gets the same gap a paragraph would.
A short standfirst, sized as a lead paragraph — the intro line under the title.
First body paragraph. Prose primitives (FText) and sciui components interleave below so the vertical rhythm between unlike neighbours is directly visible.
A second paragraph, so paragraph-to-paragraph spacing is on show too.
Body copy sitting between two sciui controls — each gets the same gap a paragraph would.
A short standfirst, sized as a lead paragraph — the intro line under the title.
First body paragraph. Prose primitives (FText) and sciui components interleave below so the vertical rhythm between unlike neighbours is directly visible.
A second paragraph, so paragraph-to-paragraph spacing is on show too.
Body copy sitting between two sciui controls — each gets the same gap a paragraph would.
An outer card paragraph.
A short standfirst, sized as a lead paragraph — the intro line under the title.
First body paragraph. Prose primitives (FText) and sciui components interleave below so the vertical rhythm between unlike neighbours is directly visible.
A second paragraph, so paragraph-to-paragraph spacing is on show too.
Body copy sitting between two sciui controls — each gets the same gap a paragraph would.
.fish-flow is an explicit rhythm scope: set --flow on it to retune. Below, the same document runs at a looser step (1.4·--gt) — the tight pairs open up proportionally, because --flow-tight is derived from --flow.
A short standfirst, sized as a lead paragraph — the intro line under the title.
First body paragraph. Prose primitives (FText) and sciui components interleave below so the vertical rhythm between unlike neighbours is directly visible.
A second paragraph, so paragraph-to-paragraph spacing is on show too.
Body copy sitting between two sciui controls — each gets the same gap a paragraph would.
.fish-col, .fish-card-body, .sciui-card-body, .sciui-disclosure-panel-inner, .sciui-tabs-content, plus the explicit .fish-flow scope. sciui is styled host-side from f-ish (same direction as --sciui-text-inset) — the sciui repo is untouched.--flow (base step, ≈ 0.7·--gt); the tight-pair step is --flow / 2, so retuning the base carries the exceptions with it. Tight pairs: kicker→title, title→lead, text→note. Everything else, prose or component, takes the base step.<div> the author adds) becomes a rhythm island — only rhythm owners space their children. Add fish-flow to such a wrapper to opt it back into rhythm. Docs (DESIGN.md/NOTES) land in Phase 7d, after the visual pass.