Skip to contentWolf-Rayet

Decision records

ADR-0058 tile is a Scope, and it is the one every scene has been typing by hand

Accepted2026-09-05Phase 4, reopened

#Context

ADR-0057 audited section B's fourteen Arbiters and left one row unresolved. Its closing section names it: *"tile does not answer it, and this record does not resolve it."* This record resolves it, and what the resolution found is not about tile.

Section B justifies its rows in one sentence — *"Each ranks one member of a set above its siblings — a selected tab, an expanded panel, a current page, a current step, a failed upload"* — and then adds a second sentence for this row alone: *"tile is an Arbiter for the same reason §3's own example list names a card."* The extra sentence is the tell. It was written because the shared argument did not stretch.

And ADR-0021 had already refused this exact move, using this exact row. Its Rejected options, on adopting Carbon's grouping as the layer assignment:

assigning tile a layer because Carbon files it under one heading would reintroduce the exact failure §3 names: tiers borrowed from a domain where the boundaries are real, applied where they are not.

Four hundred lines later, section B assigns tile a layer in one sentence about a card. The record named the trap by name and then walked into it, which is why the row survived thirty-six passes.

#The layer question has four answers, which means it is the wrong question

Carbon's tile page implements the component *"in four ways: base, clickable, selectable, and expandable"*, and the page says of the whole thing that tiles *"have no pre-set styles and are purposely flexible so product teams can determine their tile content."* It also says, of the card §3's example list names: "Carbon does not have a card pattern."

Put §3's authority question — *what authority does this have over attention?* — to the four variants separately and they answer differently:

Carbon's variantWhat Carbon says it is forWhat this system already has
clickable"prompting an action, navigating or directing to other pieces of information"Button (an action) or NavigationItem (navigating) — Emitter and Arbiter, both interactive
selectable"presenting options to a user in a structured manner"Checkbox or RadioButton — and see below
expandable"hiding and revealing a large amount of content"Accordion, whose collapsed half ADR-0040 already governs
base"high-level, short, and digestible content pieces"nothing

The selectable row is the one worth stopping on. Carbon's own feature flag for tile reads: *"Single-select tile icons have been changed from checkmark icons to radio button icons, whereas multi-select tile icons have been changed from checkmark icons to checkbox icons."* That is ADR-0028's decision — *a checkbox is square because several of its set may be picked; a radio button is a circle because exactly one may be* — arrived at independently by a team with no emission budget to defend, and shipped as a migration. A selectable tile is the rule of a set, and this system has both halves of that rule with a drift check over their geometry.

A roster row whose layer depends on which variant you picked is the ambiguous case §3 claims it does not have. It is not one component.

#Decision

tile becomes region: a Scope, in section E, at "none — allocates", and it is built. Three of Carbon's four variants are components this system already ships and are not carried again. The fourth was never a variant — it is a bounded area on a route that names a set and holds it, which is §3's Scope answer and §3's own third example, *page region*.

The row moves from section B to section E and the roster total does not change. 54 components, 29 built.

#What the audit found, which is not about tile

§3's Scope tier lists five examples: *"Panel, sidebar, page region, modal, drawer."* Four of them had components. The general one had none, and the evidence for how much that cost is in the fixture set the whole system is measured against.

Measured
Elements declaring data-wr-layer="scope" in packages/budget/fixtures144 across 53 scene files
…with a component behind them21 — 10 shell-header, 4 modal, 3 data-table, 2 shell-left-panel, 1 shell-right-panel, 1 page-header
…hand-written <section class="scope">, no component123 — 85%
…of those, carrying a class="hd" header immediately inside120 of 123
…declaring no stacking context at all38
…declaring no density at all24
Scene generators importing real components from @wolf-rayet/react29 of 30
…that also hand-write <section class="scope">29 — every one of them
Scene stylesheets carrying their own copy of the .scope rule27

Every generator in the repository renders every component through React and then types its Scope by hand. That includes make-shell-left-panel-fixtures.js, whose subject is a Scope, and make-page-header-fixtures.js, whose subject is a Scope and which was written three passes ago.

The 120 hand-written headers are the sharpest part. They read <div class="hd"><span>Detail</span><span>2 sections closed</span></div> — a name and a cardinality. This component's two governed parts were being written by hand, in that order, in nearly every scene in the set, before anything decided they were a component's parts. region's contract did not invent name and count; it found them.

#The perimeter is a defect and not only duplication

The rule those 27 stylesheets carry is this, verbatim:

.scope {
  background: var(--wr-surface-raised);
  border: 1px solid var(--wr-border-subtle);
  padding: 14px; display: flex; flex-direction: column; gap: 12px;
}

1px is 0.0625rem. field-day's no-hairline minimum is 0.125rem. That mandate is enforced in two places — verify-output.js refuses any stacking context whose border falls below it, on the grounds that *"ADR-0013 makes raised bordered in every theme"*, and refuses any component border below it on the grounds that *"the component would be unavailable in field-day"*. Both checks work by requiring an alias into the border scale, which is exactly how the rule travels. A literal in a scene stylesheet is not an alias, so neither check reaches it, and 44 themed scene runs render in field-day.

The component cannot write it. Its perimeter is var(--wr-region-level-1-weight), which the tier resolves through border-standard to 0.125rem — the minimum, exactly.

#And what a hand-written Scope can omit

measure.js reads a missing stacking declaration as base and records declared: null beside it, so the engine knows the difference and nothing acts on it. ADR-0013 says stacking is *"a property a Scope declares, never a per-component effect"*, and in 38 of 123 cases it was not declared. §5 says a Scope declares one density, and in 24 it did not.

Neither is possible in the component, by two different mechanisms and deliberately: there is no stacking prop at all — a Scope that let its caller choose would be a Scope declaring nothing — and density is required, as it is on all six Scopes before it.

#The component

A Scope, Ambient, raised, two governed parts, no variant axis, all four themes.

raised, and not for the shell surfaces' reason. A shell header and a shell panel take the bounded context because they are the frame the route is measured against. A region takes it because a Scope's boundary is the only thing that says where its ration stops. §3 puts per-Scope enforcement inside the Scope; every other Scope in the set escapes needing a drawn extent because it is identifiable by where it sits, and this one is identifiable by nothing else. ADR-0013 is what makes it affordable — a perimeter at the substrate-adjacent steps the ramps already price near zero, never a shadow — and Carbon's page reaches the same instruction from the other end: *"Tiles reside on the same plane as the page background layer and do not have elevation … Do not add a drop shadow to tiles."*

The perimeter is border-subtle where the shells' is border-strong, and that is this component's own decision rather than a copy: a shell surface is one of a kind and a region is one of several peers, and several peers each drawing a strong perimeter is a grid of boxes rather than a hierarchy. border-subtle is page-header's rule colour, the data table's rule and the table row's gridline — that record's sentence, extended: a route drawn with all four reads one hierarchy rather than four.

count is required, and that is the component's one rule. A region's subject is a set; that is what separates it from six Scopes that are each a *place*, and it is what stops this from being a container with a border. A set whose size is not stated makes two different screens identical — a region named "Alarms" drawing nothing is a quiet shift *or* a feed that stopped answering. ADR-0055 refused that collapse one layer down and gave the absence an Emitter with a required reason; this states the cardinality that makes the Emitter's claim checkable. "0 of 0" and "0 of 18" are different screens.

Its subtree may hold the View's demand, and this is ADR-0053's complement arriving at its ordinary case. page-header was the first component to say so and said it about a *borrowed* demand — one lifted out of a region the reader cannot see, which is ADR-0040's remedy and exceptional by construction. §3 puts per-Scope enforcement in the Scope and ADR-0010 rations demands across Scopes within a View. Both sentences are about this component, and until now neither had an ordinary subject.

#Measured: scene 54 against scene 51

Scene 54 is scene 51 with two elements changed and nothing else — the same shell header, the same page header, the same lifted demand, the same two closed sections, the same three readings, the same theme and density. The two Scopes holding them were hand-written <section> elements and are now the component.

Scope5154Δ
#shell0.0583070.0583070.000000
#route-header0.0340280.0340280.000000
#scope-detail0.0565520.059439+0.002887
#scope-line0.0467460.048777+0.002031

The two zeros are the control and they are why the other two numbers mean anything: nothing outside the changed elements moved, to six decimals. The boundary costs +0.0029 and +0.0020 against a field-night ceiling of 0.129 — 2.2% and 1.6% — for a perimeter twice as wide, drawn at the width field-day requires and the literal never was.

The demand stays in the page header rather than moving into a region. Moving it would have shown the component doing the more interesting thing and changed two variables at once; REGION_MAY_HOLD_A_DEMAND is asserted in the type fixture and shown in the workbench instead.

#Rejected options

Drop tile entirely, as ADR-0057 dropped the three lists. The consistent-looking move, and the one the audit's own precedent points at: three variants resolve to built components and a row that scatters is not a row. It lost on the fourth variant, which resolves to nothing and turned out to be the most-used undeclared thing in the repository. A drop would have recorded that Carbon's tile is four components this system has, and been wrong by one — the one the fixture set has been writing by hand 123 times.

Keep tile as an Arbiter and build it as a container that ranks its children. What the roster said, and it has a real argument: a selectable tile group does rank one member above its siblings, which is section B's own test passed. It lost because that is the *selectable* variant and it is RadioButton's rule of the set, which Carbon is itself migrating to. A component built for a job another component already does, justified by one of its four variants, is the scenery §11 warns about with a layer label on it.

Build it as a Scope and keep the name tile. Cheaper on the roster: one row, one slug, no rename. It lost for alert/toast's reason. Carbon's tile names four things and this is one of them; carrying the noun would import the ambiguity the record exists to remove, and a reader who reached for Tile expecting a clickable card would find a container that cannot be clicked. §3's own word for it is *page region*, and this system already uses "the region maximum" throughout to mean the ceiling inside one Scope — so the component and the ration are named the same thing on purpose.

Rewrite the 123 hand-written Scopes to use the component. The finding says they should be, and it is the wrong pass to do it in. Every one of them sits under a committed baseline calibrated on the markup as it is; changing them would move 123 Scope values across two platforms in the same commit that introduced the thing being measured, and there would be no control. Scene 54 changes two of them, holds everything else fixed, and reports the delta. The rest is named below and is its own unit.

Make count optional. The convenient shape, and the one every container component in every system has. It lost because it is the shape that lets a caller silently opt out of the one sentence this component exists to say, and because ADR-0055 had just refused the same collapse one layer down. An optional cardinality is a container with a border.

#Consequences

Roster 54, built 29. The row moved rather than left, so no count changed. Section B is ten rows and its justifying sentence now covers all of them.

§5's second Scope depth is still unoccupied, and this record does not fix it. All seven Scopes are ScopeNode<…, 1>, and ScopeChildren admits a ScopeNode<…, 2> that no component is — so no Scope can legally nest inside another today, and §5's *"Scopes do not nest more than two deep"* has never had an instance to be about. A region inside a shell panel is the obvious first one. It is a change to core's node model and is named here rather than half-done, which is ADR-0050's habit for a debt a record cannot pay in the pass that finds it.

123 Scopes are owed the component and are not getting it this pass, for the reason the rejected option gives. What that migration is worth is now a number rather than a preference: 2.2% and 1.6% of the field-night ceiling per region, and a perimeter that stops being half of field-day's minimum.

Six scenes have no themed sibling — 48 through 53, and now 54. The RETHEMED list in make-themed-fixtures.js ends at 47 and the themed set stands at 140. This scene is the third raised context in the repository and the first with two of them on one screen, which is exactly what that tool's own comment on scenes 44–46 says to watch: whether the context keeps grounding on the theme substrate rather than acquiring a luminance of its own. Closing that gap is a unit, not a line.

No new check kind, and playbook §10 stands at 26. What grows is the fixture set by one scene and the discipline table by one row.

linux-x64 is short by exactly the one new scene, four Scope values, and the follow-up is the same one every component pass has taken.

#Measured

  • ADR-0021's Rejected options, naming tile while refusing Carbon's grouping — quoted above, *disk*.
  • Carbon's tile usage page: four variants, "Carbon does not have a card pattern", "no pre-set styles", and the feature flag migrating selection to checkbox and radio icons — carbon-design-system/carbon-website src/pages/components/tile/usage.mdx, 2026-09-05.
  • 144 Scope declarations across 53 scene files; 123 with no component, 85%; 120 carrying a name-and-count header; 38 with no stacking context; 24 with no density.
  • 29 of 30 scene generators import real components and hand-write their Scopes; 27 scene stylesheets carry their own .scope rule.
  • The literal 1px = 0.0625rem against field-day's 0.125rem minimum; 44 themed runs in field-day.
  • Scene 54 against scene 51: two Scopes unchanged to six decimals, +0.002887 and +0.002031 against a 0.129 ceiling.
  • region: 14 component tier tokens, 8 APCA floor checks all met, worst |Lc| 20 against a field-night floor of 15.
  • Roster 54, built 28 → 29. pnpm battery — the verdict this pass is measured by.