@vitreajs/vitrea 0.1.1 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -180,13 +180,40 @@ type InteractionState = (typeof INTERACTION_STATES)[number];
180
180
  * source — are *not* diagnostics. Those throw `GlassSceneError`, because
181
181
  * continuing past them would leave a half-built scene. Diagnostics carry the
182
182
  * recoverable, per-frame, policy-level findings instead.
183
+ *
184
+ * ## One channel, several code spaces
185
+ *
186
+ * The channel is generic over its code union and core's own union is only its
187
+ * default instantiation. That is what lets the browser layer — which detects
188
+ * things core cannot name, like a host placed outside its plane — have a code
189
+ * space of its own without a second copy of the machinery, and it is why the
190
+ * dedupe rule, the retention rule and the key separator have exactly one
191
+ * definition in the workspace (Decision Log #21(b), #23(c)).
192
+ *
193
+ * Two properties are load-bearing and deliberately *not* generalised:
194
+ *
195
+ * - **A diagnostic carries no origin tag.** Which code space a finding came
196
+ * from is the channel's business, added on the way out to a host's sink, not
197
+ * a field every emitter has to write. `platform-web`'s `layer-model.ts` is
198
+ * the proof: it is a pure function that takes a bare `report` callback, and
199
+ * an origin tag in the payload would have rewritten every emitter in it.
200
+ * - **`report` stays assignable to `(d) => void`.** Checkers take the
201
+ * capability to report, never the channel, so a module that emits findings
202
+ * knows nothing about retention or dedupe.
183
203
  */
184
204
  type DiagnosticSeverity = "warning" | "error";
185
205
  /** Everything core can report. Owned here so a host can switch exhaustively. */
186
- declare const DIAGNOSTIC_CODES: readonly ["same-plane-overlap", "variant-mixing", "merge-distance-below-padding", "group-proxy-overlap", "clear-variant-needs-dimming", "foreground-mode-illegal", "foreground-rate-clamped", "backdrop-hint-out-of-range", "backdrop-hint-redundant-estimator", "reduced-transparency-undetectable", "frame-phase-violation"];
206
+ declare const DIAGNOSTIC_CODES: readonly ["same-plane-overlap", "variant-mixing", "tint-mixing", "merge-distance-below-padding", "group-proxy-overlap", "clear-variant-needs-dimming", "foreground-mode-illegal", "foreground-rate-clamped", "backdrop-hint-out-of-range", "backdrop-hint-redundant-estimator", "reduced-transparency-undetectable", "frame-phase-violation"];
187
207
  type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
188
- interface Diagnostic {
189
- readonly code: DiagnosticCode;
208
+ /**
209
+ * One finding, over whichever code space the channel was opened on.
210
+ *
211
+ * `Code` defaults to core's own union, so `Diagnostic` unqualified still means
212
+ * exactly what it meant before the channel became generic and every existing
213
+ * annotation of it still reads.
214
+ */
215
+ interface Diagnostic<Code extends string = DiagnosticCode> {
216
+ readonly code: Code;
190
217
  readonly severity: DiagnosticSeverity;
191
218
  /**
192
219
  * The ids this finding is about — group, node, or source. Together with
@@ -196,21 +223,21 @@ interface Diagnostic {
196
223
  readonly subjects: readonly string[];
197
224
  readonly message: string;
198
225
  }
199
- type DiagnosticSink = (diagnostic: Diagnostic) => void;
200
- interface DiagnosticsChannel {
201
- report(diagnostic: Diagnostic): void;
226
+ type DiagnosticSink<Code extends string = DiagnosticCode> = (diagnostic: Diagnostic<Code>) => void;
227
+ interface DiagnosticsChannel<Code extends string = DiagnosticCode> {
228
+ report(diagnostic: Diagnostic<Code>): void;
202
229
  /** Findings retained since construction or the last `clear()`. */
203
- readonly reported: readonly Diagnostic[];
230
+ readonly reported: readonly Diagnostic<Code>[];
204
231
  /** Forget what was seen, so a condition that returns is reported again. */
205
232
  clear(): void;
206
233
  }
207
- interface DiagnosticsChannelOptions {
234
+ interface DiagnosticsChannelOptions<Code extends string = DiagnosticCode> {
208
235
  /** Where findings go. Omitted in tests and in hosts that only read `reported`. */
209
- readonly sink?: DiagnosticSink;
236
+ readonly sink?: DiagnosticSink<Code>;
210
237
  /** Collapse repeats of the same code+subjects. Default true. */
211
238
  readonly dedupe?: boolean;
212
239
  }
213
- declare function createDiagnosticsChannel(options?: DiagnosticsChannelOptions): DiagnosticsChannel;
240
+ declare function createDiagnosticsChannel<Code extends string = DiagnosticCode>(options?: DiagnosticsChannelOptions<Code>): DiagnosticsChannel<Code>;
214
241
 
215
242
  /**
216
243
  * Accessibility policy (§Accessibility policy, plus the Reduced Motion
@@ -415,6 +442,26 @@ declare const ACCESSIBILITY_PRECEDENCE: readonly ["reducedMotion", "reducedTrans
415
442
  */
416
443
  declare function resolveAccessibilityPolicy(system: SystemAccessibilityPreferences, overrides?: AccessibilityOverrides, diagnostics?: DiagnosticsChannel): ResolvedAccessibilityPolicy;
417
444
 
445
+ /**
446
+ * The refraction ladder, weakest rung first.
447
+ *
448
+ * Two independent things cap refraction: the accessibility policy's ceiling
449
+ * (core's `ResolvedMaterialPolicy.refraction`, a regime — `nominal | reduced |
450
+ * none`) and the group's resolved capability state (X2's `RefractionQuality` —
451
+ * `true | approximate | none`, what the sampling backend can actually deliver).
452
+ * **Renderers honour the lower of the two** (Decision Log #19), which is only a
453
+ * meaningful sentence against an ordering — this one.
454
+ *
455
+ * Written out rather than derived from `RefractionQuality`, because that type's
456
+ * own declaration order is not an ordering.
457
+ */
458
+ declare const REFRACTION_LADDER: readonly ["none", "approximate", "true"];
459
+ /**
460
+ * X2's capability-derived refraction level (`core/src/state.ts` re-exports this
461
+ * name). Derived from the ladder so the rungs and the type cannot drift apart.
462
+ */
463
+ type RefractionQuality = (typeof REFRACTION_LADDER)[number];
464
+
418
465
  /**
419
466
  * X2 — the resolved-state model (§Backdrop & analysis contracts).
420
467
  *
@@ -425,10 +472,10 @@ declare function resolveAccessibilityPolicy(system: SystemAccessibilityPreferenc
425
472
  *
426
473
  * C1 ships the shape; C4 ships the resolver.
427
474
  */
475
+
428
476
  type ConfiguredSource = "texture" | "dom";
429
477
  type ActiveRenderer = "webgpu" | "css";
430
478
  type SamplingBackend = "gpu-texture" | "css-backdrop" | "none";
431
- type RefractionQuality$1 = "true" | "approximate" | "none";
432
479
  type AnalysisQuality = "exact" | "hint" | "none";
433
480
  type GroupHealth = "ok" | "demoted";
434
481
  declare const DEMOTION_REASONS: readonly ["no-webgpu", "no-backdrop-filter", "tainted-source", "incompatible-texture", "no-texture-supplied", "device-lost", "probe-failed", "governor"];
@@ -439,7 +486,7 @@ interface GlassGroupState {
439
486
  /** What is actually drawing. */
440
487
  readonly activeRenderer: ActiveRenderer;
441
488
  readonly samplingBackend: SamplingBackend;
442
- readonly refraction: RefractionQuality$1;
489
+ readonly refraction: RefractionQuality;
443
490
  readonly analysis: AnalysisQuality;
444
491
  readonly health: GroupHealth;
445
492
  readonly demotionReason?: DemotionReason;
@@ -797,6 +844,53 @@ interface FrameInfo {
797
844
 
798
845
  declare const MATERIAL_VARIANTS$1: readonly ["regular", "clear"];
799
846
  type MaterialVariant$1 = (typeof MATERIAL_VARIANTS$1)[number];
847
+ /**
848
+ * An author's tint seed, sRGB-encoded, 0..1 per channel.
849
+ *
850
+ * Encoded rather than linear because this is the number the author wrote: a CSS
851
+ * colour, parsed. The conversion into the working space belongs to whichever
852
+ * tier is drawing, and core carries no colour maths.
853
+ */
854
+ type TintColor = readonly [r: number, g: number, b: number];
855
+ /**
856
+ * §Material tint — the author-facing half of Apple's `Glass.tint(_:)`.
857
+ *
858
+ * **A tint is a seed, not a fill.** Apple states the mechanism plainly:
859
+ * "selecting a color generates a range of tones that are **mapped to content
860
+ * brightness underneath** the tinted element… changing its hue, brightness and
861
+ * saturation depending on what's behind without deviating too much from the
862
+ * intended color" (WWDC25 session 219). A flat overlay of the seed is the
863
+ * failure Apple names in the same session — "completely opaque and breaks the
864
+ * visual character of Liquid Glass" — so this value is carried to the renderers
865
+ * as a seed and tone-mapped there, per pixel, against the backdrop the material
866
+ * is already sampling.
867
+ *
868
+ * Two axes, kept apart on purpose, because Apple's own vocabulary overloads the
869
+ * word:
870
+ *
871
+ * - **This is the colour axis.** It says what colour the material's tint layer
872
+ * is. It never changes how much of that layer there is.
873
+ * - The **alpha axis** — how opaque the tint layer is — is the material's
874
+ * calibrated `tintAlpha`, the same quantity reduced transparency lifts. That
875
+ * is where the *user's* system preference lives (iOS 26.1's Clear/Tinted
876
+ * toggle "increases the opacity of Liquid Glass and adds more contrast"; OS
877
+ * 27's slider runs the same axis continuously, "ultra clear to fully
878
+ * tinted"). A future reference migration therefore lands on the occlusion
879
+ * axis and cannot collide with an author's tint.
880
+ *
881
+ * `strength` is the author's own subtlety knob and comes from the seed colour's
882
+ * alpha — `rgba(255, 149, 0, 0.5)` is a half-strength orange, exactly as
883
+ * `Color.orange.opacity(0.5)` is in SwiftUI. It says how far the material's tint
884
+ * colour moves from its neutral (profile) tint toward the tone, and at 0 the
885
+ * material is byte-identical to an untinted one.
886
+ */
887
+ interface GlassTint {
888
+ readonly color: TintColor;
889
+ /** How far the material's tint moves toward the tone, 0..1. */
890
+ readonly strength: number;
891
+ }
892
+ /** A tint with its channels clamped into range. `strength` defaults to fully tinted. */
893
+ declare function glassTint(color: TintColor, strength?: number): GlassTint;
800
894
  /** The scrim laid beneath clear glass so foreground content stays legible. */
801
895
  interface DimmingPolicy {
802
896
  /** Scrim opacity, 0..1. */
@@ -810,11 +904,13 @@ interface DimmingPolicy {
810
904
  * satisfy, not to be correct.
811
905
  */
812
906
  declare const DEFAULT_CLEAR_DIMMING: DimmingPolicy;
813
- /** A group's material defaults. A node inherits `variant` when it declares none. */
907
+ /** A group's material defaults. A node inherits `variant` and `tint` when it declares none. */
814
908
  interface MaterialProfile$1 {
815
909
  readonly variant: MaterialVariant$1;
816
910
  /** Required for any clear surface in the group. */
817
911
  readonly dimming?: DimmingPolicy;
912
+ /** Group-wide tint seed. A node overrides it, or clears it with `null`. */
913
+ readonly tint?: GlassTint;
818
914
  }
819
915
  interface ResolvedMaterial {
820
916
  readonly variant: MaterialVariant$1;
@@ -822,15 +918,45 @@ interface ResolvedMaterial {
822
918
  readonly adaptation: "adaptive" | "constrained";
823
919
  /** Present exactly when `variant` is `"clear"`. */
824
920
  readonly dimming?: DimmingPolicy;
921
+ /** Absent when the surface is untinted, or when its tint has no strength. */
922
+ readonly tint?: GlassTint;
825
923
  }
826
924
  interface MaterialRequest {
827
925
  readonly variant: MaterialVariant$1;
828
926
  readonly dimming?: DimmingPolicy;
927
+ /** The tint this surface resolved to — its own, or the group's. */
928
+ readonly tint?: GlassTint | null;
829
929
  /** Named in diagnostics; also the dedupe subject. */
830
930
  readonly nodeId?: string;
831
931
  readonly diagnostics?: DiagnosticsChannel;
832
932
  }
833
933
  declare function resolveMaterial(request: MaterialRequest): ResolvedMaterial;
934
+ interface TintMixingCheck {
935
+ readonly groupId: string;
936
+ readonly members: readonly {
937
+ readonly nodeId: string;
938
+ readonly tint?: GlassTint;
939
+ }[];
940
+ readonly diagnostics?: DiagnosticsChannel;
941
+ }
942
+ /**
943
+ * Report a group whose members ask for **different** tint seeds.
944
+ *
945
+ * A group is one sampling region and one optics pass, so the GPU tier carries
946
+ * one seed per group and a per-pixel strength — which is exactly enough for the
947
+ * composition Apple's guidance describes ("apply color to the background rather
948
+ * than to symbols… refrain from adding color to the background of multiple
949
+ * controls"): one emphasised control inside a toolbar of plain ones. Two
950
+ * different hues in one group is outside that, and the two tiers would then
951
+ * disagree — the CSS tier styles each host element on its own and can honour
952
+ * both. So it warns and changes nothing, on the same reasoning as
953
+ * `checkVariantMixing`: coercing one of the two would silently discard an
954
+ * author's intent.
955
+ *
956
+ * Untinted members are not a mix. They are the ordinary case the mechanism is
957
+ * for, and their strength is simply zero.
958
+ */
959
+ declare function checkTintMixing(check: TintMixingCheck): boolean;
834
960
  interface VariantMixingCheck {
835
961
  readonly groupId: string;
836
962
  readonly members: readonly {
@@ -871,7 +997,7 @@ interface ZSlot {
871
997
  readonly order: number;
872
998
  }
873
999
  /** Viewport-space rectangle in CSS px, measured by platform-web and handed in as data. */
874
- interface Rect {
1000
+ interface Rect$1 {
875
1001
  readonly x: number;
876
1002
  readonly y: number;
877
1003
  readonly width: number;
@@ -880,15 +1006,38 @@ interface Rect {
880
1006
  /** Back-to-front ordering: every base node before every overlay node, then by `order`. */
881
1007
  declare function compareZSlot(a: ZSlot, b: ZSlot): number;
882
1008
  /** Smallest rect containing both. */
883
- declare function unionRect(a: Rect, b: Rect): Rect;
1009
+ declare function unionRect(a: Rect$1, b: Rect$1): Rect$1;
884
1010
  /** Grow a rect outwards on every side — how a group's proxy gets its padding. */
885
- declare function inflateRect(rect: Rect, by: number): Rect;
1011
+ declare function inflateRect(rect: Rect$1, by: number): Rect$1;
1012
+ /**
1013
+ * The part of `rect` that survives every clip in the chain.
1014
+ *
1015
+ * A surface's border box is measured unclipped — `getBoundingClientRect` reports
1016
+ * the box wherever it is, whether or not an `overflow: scroll` ancestor is
1017
+ * actually showing it — so the box alone says nothing about what is visible. The
1018
+ * clip chain, measured alongside it, is what turns the box into the region the
1019
+ * surface can paint in (Decision Log #41(k)).
1020
+ *
1021
+ * A fully scrolled-out surface intersects to zero extent, and every consumer in
1022
+ * here already treats a zero-extent rect as "contributes nothing": `rectsOverlap`
1023
+ * refuses it, and the proxy geometry skips it as unmeasured. That is deliberate
1024
+ * — it means "scrolled out of view" needs no special case anywhere downstream.
1025
+ *
1026
+ * Rects only, and the approximation is stated rather than implied: a rounded
1027
+ * clipping ancestor is carried as its bounding box, so a surface tucked into a
1028
+ * rounded scroller's corner is treated as slightly more visible than it is. The
1029
+ * error is bounded by the ancestor's corner radius and always errs towards
1030
+ * reporting *more* surface, which is the safe direction for every consumer here
1031
+ * — an overlap check that over-reports warns about something real-ish, one that
1032
+ * under-reports misses a genuine double-filter.
1033
+ */
1034
+ declare function clipRect(rect: Rect$1, clip: readonly Rect$1[] | undefined): Rect$1;
886
1035
  /**
887
1036
  * Positive-area intersection. Touching edges are not an overlap — adjacent
888
1037
  * surfaces in a toolbar are the common case and are legal. A degenerate rect
889
1038
  * (an unmeasured or collapsed host) overlaps nothing.
890
1039
  */
891
- declare function rectsOverlap(a: Rect, b: Rect): boolean;
1040
+ declare function rectsOverlap(a: Rect$1, b: Rect$1): boolean;
892
1041
 
893
1042
  /**
894
1043
  * The scene model (§Core model): the three registries and everything that
@@ -1024,6 +1173,28 @@ interface GlassGroupRecord {
1024
1173
  readonly state?: GlassGroupState;
1025
1174
  /** Per-group governor override; falls back to the scene-wide pressure. */
1026
1175
  readonly governor?: GovernorPressure;
1176
+ /**
1177
+ * Per-group platform probe override; falls back to the scene-wide probe.
1178
+ *
1179
+ * Most of `PlatformProbe` genuinely is scene-wide — there is one device per
1180
+ * root, and whether the engine has `backdrop-filter` is a fact about the
1181
+ * engine. `backdropProxyConformance` is the exception, and S1 measured why:
1182
+ * the backdrop-root audit is per group, "not per document, because different
1183
+ * groups can sit under different ancestors". A group whose proxy chain is
1184
+ * re-rooted must demote alone.
1185
+ */
1186
+ readonly platform?: PlatformProbe;
1187
+ }
1188
+ /**
1189
+ * X8 rider 2: this surface is a level set of another surface's field, inset by a
1190
+ * fixed distance — a segmented control's indicator inside its track, drawn as
1191
+ * one field rather than two shapes that happen to nest.
1192
+ */
1193
+ interface ConcentricParent {
1194
+ /** The parent surface. Must be registered, and must share this node's group. */
1195
+ readonly nodeId: string;
1196
+ /** CSS px inward from the parent's contour. */
1197
+ readonly inset: number;
1027
1198
  }
1028
1199
  interface GlassNodeDescriptor {
1029
1200
  readonly id: string;
@@ -1031,8 +1202,33 @@ interface GlassNodeDescriptor {
1031
1202
  readonly shapeFamily: ShapeFamily;
1032
1203
  readonly shape: ShapeChannels;
1033
1204
  readonly zSlot: ZSlot;
1205
+ /**
1206
+ * Which of geometry's two corner references this shape is fit against
1207
+ * (Decision Log #22(a) — two separate fits, not two points on one axis).
1208
+ * Defaults to `"apple-continuous"` at the renderer.
1209
+ *
1210
+ * A scene-model field since Decision Log #23(c). In v1 it was a render input
1211
+ * the browser layer never set, so a shape authored on the Figma smoothing axis
1212
+ * was silently resolved against the Apple fit, and a binding that wanted to
1213
+ * refuse a cross-reference morph had to mirror geometry's private mapping to
1214
+ * do it. The reference travels with the shape now.
1215
+ */
1216
+ readonly reference?: CornerReference;
1217
+ /**
1218
+ * X8 rider 2's parent edge, likewise a scene-model field since #23(c).
1219
+ *
1220
+ * The link is validated here rather than at draw time: an unknown parent, a
1221
+ * parent in another group and a cycle are all refusals at registration, where
1222
+ * the caller that made the mistake is still on the stack.
1223
+ */
1224
+ readonly concentricOf?: ConcentricParent;
1034
1225
  /** Inherits the group's material profile when absent. */
1035
1226
  readonly variant?: MaterialVariant$1;
1227
+ /**
1228
+ * Overrides the group's tint seed. `null` clears an inherited one, the way
1229
+ * `Glass.tint(nil)` does; absent inherits.
1230
+ */
1231
+ readonly tint?: GlassTint | null;
1036
1232
  readonly interaction?: InteractionState;
1037
1233
  /** Overrides the group's adaptation for this surface. */
1038
1234
  readonly foreground?: ForegroundAdaptation;
@@ -1040,9 +1236,9 @@ interface GlassNodeDescriptor {
1040
1236
  interface GlassNodeRecord {
1041
1237
  readonly descriptor: GlassNodeDescriptor;
1042
1238
  /** Measured by platform-web in the read phase; absent until then. */
1043
- readonly bounds?: Rect;
1239
+ readonly bounds?: Rect$1;
1044
1240
  /** Ancestor clip chain, viewport space. */
1045
- readonly clip?: readonly Rect[];
1241
+ readonly clip?: readonly Rect$1[];
1046
1242
  }
1047
1243
  /** One pyramid rebuild. `groupIds` is why there is one request and not one per group. */
1048
1244
  interface BackdropRebuildRequest {
@@ -1090,7 +1286,10 @@ interface PlaneOverlap {
1090
1286
  readonly plane: GlassPlane;
1091
1287
  readonly nodeIds: readonly [string, string];
1092
1288
  }
1093
- /** Two groups whose padded backdrop proxies would overlap in one plane (X1). */
1289
+ /**
1290
+ * Two groups close enough in one plane that one group's padded proxy would
1291
+ * sample the pixels the other one paints (X1).
1292
+ */
1094
1293
  interface ProxyOverlap {
1095
1294
  readonly plane: GlassPlane;
1096
1295
  readonly groupIds: readonly [string, string];
@@ -1136,8 +1335,26 @@ interface GlassScene {
1136
1335
  glassNode(id: string): GlassNodeRecord | undefined;
1137
1336
  nodesOfGroup(groupId: string): readonly GlassNodeRecord[];
1138
1337
  /** Measured viewport geometry, from the read phase. */
1139
- setNodeBounds(id: string, bounds: Rect, clip?: readonly Rect[]): void;
1140
- setPlatformProbe(probe: PlatformProbe): void;
1338
+ setNodeBounds(id: string, bounds: Rect$1, clip?: readonly Rect$1[]): void;
1339
+ /**
1340
+ * Scene-wide by default; per group when `groupId` is given.
1341
+ *
1342
+ * The per-group form exists for `backdropProxyConformance` (Decision Log
1343
+ * #21(a), #23(c)). S1's backdrop-root audit is per group — different groups
1344
+ * sit under different ancestors — so a scene-wide-only probe forced the
1345
+ * browser layer either to demote every group when one failed, or to bypass
1346
+ * `resolve()` and call the pure resolver itself with the verdict folded in.
1347
+ * It chose the second, honestly and in the open, and this setter is what
1348
+ * retires it: with the verdict in the scene, `ResolvedGroup.state` and the
1349
+ * host's per-group answer are the same answer again.
1350
+ *
1351
+ * A per-group probe REPLACES the scene-wide one for that group rather than
1352
+ * merging with it, exactly as `setGovernorPressure` does. Merging would be a
1353
+ * second precedence rule sitting beside `REASON_PRECEDENCE`, and the caller
1354
+ * that knows the group's verdict is the same caller that holds the scene-wide
1355
+ * probe it was derived from.
1356
+ */
1357
+ setPlatformProbe(probe: PlatformProbe, groupId?: string): void;
1141
1358
  setSourceProbe(sourceId: string, probe: SourceProbe): void;
1142
1359
  /** Scene-wide by default; per group when `groupId` is given. */
1143
1360
  setGovernorPressure(pressure: GovernorPressure, groupId?: string): void;
@@ -1171,8 +1388,8 @@ interface GlassScene {
1171
1388
  checkSamePlaneOverlap(): readonly PlaneOverlap[];
1172
1389
  /**
1173
1390
  * The cross-group half of X1's proxy geometry. `mergeDistance` only unions
1174
- * members *within* a group, so two neighbouring groups can still put two
1175
- * padded proxies over the same pixels — which S1 measured double-filtering.
1391
+ * members *within* a group, so a neighbouring group's proxy can still sample
1392
+ * the pixels this one paints — which S1 measured double-filtering.
1176
1393
  */
1177
1394
  checkGroupProxyOverlap(): readonly ProxyOverlap[];
1178
1395
  }
@@ -1353,15 +1570,15 @@ type Rgb = readonly [r: number, g: number, b: number];
1353
1570
  * **The lower of the two wins**, and this module folds them into one scalar before
1354
1571
  * anything reaches a uniform, so the shader has no way to honour the wrong one.
1355
1572
  *
1356
- * The ordering mirrors `platform-web`'s `REFRACTION_LADDER`, which serves the CSS
1357
- * tier. It is restated rather than imported because this package sits *below*
1358
- * core in the dependency graph and platform-web sits above it. That the two
1359
- * copies must agree is a real (if small) seam — see the note in the C6 report.
1573
+ * The ordering is `@vitrea/policy`'s, and so is the fold. It used to be restated
1574
+ * here — this package sits *below* core in the dependency graph and platform-web
1575
+ * sits above it, so for most of v1 there was no module both tiers could see and
1576
+ * the CSS tier carried a second copy. Decision Log #23(d) closed that seam by
1577
+ * putting the ladder in a pure leaf underneath everything, which the renderer can
1578
+ * depend on directly (alongside `@vitrea/geometry`) with no cycle to close. The
1579
+ * two copies can no longer disagree because there is only one.
1360
1580
  */
1361
1581
 
1362
- /** X2's `RefractionQuality`, restated. Weakest first — the declaration order IS the ladder. */
1363
- declare const REFRACTION_LADDER: readonly ["none", "approximate", "true"];
1364
- type RefractionQuality = (typeof REFRACTION_LADDER)[number];
1365
1582
  /**
1366
1583
  * The slice of core's `ResolvedAccessibilityPolicy["material"]` the renderer
1367
1584
  * reads. core's type is assignable to this; a test pins that.
@@ -1401,6 +1618,96 @@ interface MaterialRim {
1401
1618
  readonly rimWidth: number;
1402
1619
  readonly rimAlpha: number;
1403
1620
  }
1621
+ /**
1622
+ * The outer shadow (W8) — the material's own occlusion of the backdrop *outside*
1623
+ * its contour, and the largest single facet the project has measured.
1624
+ *
1625
+ * Not the same quantity as `MaterialOptics.shadowDepth`/`shadowAlpha`, which are
1626
+ * the *inner* shadow: that one darkens the material's own body near its contour,
1627
+ * this one darkens what is behind and beside the surface. Profile-level rather
1628
+ * than per-variant, because the bed measures it per profile and never varied the
1629
+ * variant.
1630
+ *
1631
+ * ## The mechanism, as measured
1632
+ *
1633
+ * The reference's shadow is the component's OWN rounded silhouette, outset by
1634
+ * `spreadPx`, translated down by `offsetPx`, blurred by a Gaussian of standard
1635
+ * deviation `sigmaPx`, and applied MULTIPLICATIVELY: the backdrop keeps
1636
+ * `1 − occlusion·falloff` of its own light. Fitted in two dimensions against the
1637
+ * active bed, that model reproduces the reference to an RMS of 0.0021 in
1638
+ * occlusion over 142,550 pixels on the finest cell, and the same three lengths
1639
+ * describe every profile, backdrop, span and scale in the bed.
1640
+ *
1641
+ * **Multiplicative, and not additively.** Mirrored pixel pairs either side of a
1642
+ * capsule over the `photo` backdrop see the same shadow over different backdrop
1643
+ * luminances: the darkening's ratio tracks the backdrop's ratio to 4.5% while a
1644
+ * constant-subtraction model misses by 79% of the signal. So the shadow is
1645
+ * analytically INVISIBLE over black — `dark-solid` cells are byte-identical to
1646
+ * their background — and that property is what both tiers reproduce exactly,
1647
+ * because a fully transparent black composited over anything leaves it alone and
1648
+ * black times anything is black.
1649
+ *
1650
+ * ## Lengths, in points
1651
+ *
1652
+ * Every length below is in CSS px and the 2× bed proves it: `sigmaPx` measures
1653
+ * 15.5 at 1× and 31.0 at 2× device px, `offsetPx` 7.9 and 15.8. A shadow
1654
+ * specified in points is what doubles that way.
1655
+ *
1656
+ * They are also SPAN-INVARIANT, which is a positive measurement rather than an
1657
+ * absence: across spans of 32, 44, 96 and 160 px the fitted σ stays within
1658
+ * 15.4…15.9 and the offset within 6.9…8.1. The size law reaches the amplitude
1659
+ * (`sizeGain`) and nothing else.
1660
+ */
1661
+ interface MaterialOuterShadow {
1662
+ /** Downward translation of the shadow's silhouette, CSS px. */
1663
+ readonly offsetPx: number;
1664
+ /** Gaussian σ the silhouette is blurred by, CSS px. A `box-shadow` blur is 2σ. */
1665
+ readonly sigmaPx: number;
1666
+ /** Outward spread of the silhouette before the blur, CSS px. */
1667
+ readonly spreadPx: number;
1668
+ /**
1669
+ * Peak occlusion: the fraction of the backdrop's own LINEAR light removed deep
1670
+ * inside the shadow. Zero stands the whole facet down, pad and all.
1671
+ */
1672
+ readonly occlusion: number;
1673
+ /**
1674
+ * What reduced transparency does to `occlusion` — MEASURED, not assumed, which
1675
+ * is what the charter asked for before the fold was written.
1676
+ *
1677
+ * The reference's shadow under `reduce transparency` is the same shadow at
1678
+ * 0.566 of the amplitude: 0.1830/0.3259, 0.1884/0.3309 and 0.1882/0.3314 on the
1679
+ * three structured backdrops at a 44 px span, with σ, offset and spread
1680
+ * unmoved. It does not vanish and it does not intensify.
1681
+ *
1682
+ * The `increased contrast` reference reproduces the reduced-transparency
1683
+ * amplitude to four decimals (0.1830, 0.1884, 0.1882 — the same numbers), which
1684
+ * is Decision Log 8's finding again: macOS force-couples the two toggles, so the
1685
+ * contrast reference IS the reduced-transparency state and the bed cannot
1686
+ * separate them. The fold therefore keys on `frost`, the axis reduced
1687
+ * transparency alone sets, rather than on the contrast axes it would be
1688
+ * indistinguishable on here.
1689
+ */
1690
+ readonly reducedTransparencyOcclusion: number;
1691
+ /**
1692
+ * The size law's grip on the amplitude: the fraction of the REMAINING
1693
+ * transparency a full-thickness surface's shadow closes, on
1694
+ * `sizeOcclusionGain`'s relative form.
1695
+ *
1696
+ * Ships at 0 — the identity — and the reason is a measurement rather than an
1697
+ * absence of one. Fitted per scene at a frozen geometry, the amplitude's span
1698
+ * dependence points in OPPOSITE directions in the two colour schemes: light
1699
+ * standard falls from 0.326 to 0.196 between a 44 px and a 96 px span over
1700
+ * `photo` (and 0.331 → 0.285 over `checkerboard`, 0.331 → 0.245 over
1701
+ * `hc-text`), while dark standard RISES from 0.060 to 0.177 to 0.274 across 44,
1702
+ * 96 and 160 px. Under reduced transparency it is flat (0.183, 0.192, 0.165).
1703
+ * One monotone gain on one thickness curve cannot be all three, and any
1704
+ * non-zero value fitted to one scheme is wrong in the other — the same shape of
1705
+ * finding Decision Log 13 recorded for W7's curve ("surface size is its own
1706
+ * axis"). The seam ships so the cascade can fit it if a two-axis rework lands;
1707
+ * the value stays at the identity until something can identify it.
1708
+ */
1709
+ readonly sizeGain: number;
1710
+ }
1404
1711
  /**
1405
1712
  * Every number the material runs on, in one place.
1406
1713
  *
@@ -1432,24 +1739,138 @@ interface MaterialProfile {
1432
1739
  */
1433
1740
  readonly refractionScale: Readonly<Record<RefractionQuality, number>>;
1434
1741
  /**
1435
- * The size-parameterised lens depth — parent acceptance #2's mechanism.
1742
+ * **The size law's one curve** — the span band over which the material stops
1743
+ * reading as a thin sheet and starts reading as a thick slab (W2).
1436
1744
  *
1437
- * Below `lensSpanMin` a surface gets its authored thickness and nothing more;
1438
- * above `lensSpanMax` it gets `lensSizeGainMax` times it. The final clamp to the
1439
- * shorter *half* extent is what keeps a small control from being all lens: a
1440
- * 24 px-tall button cannot bend more than 12 px of backdrop however thick it is
1441
- * authored.
1745
+ * Apple states one mechanism and lists its consequences: as glass "morphs to
1746
+ * larger sizes… its material characteristics change to simulate a thicker, more
1747
+ * substantial material. It casts deeper, richer shadows, has more pronounced
1748
+ * lensing and refraction effects, and a softer scattering of light" (S219). One
1749
+ * mechanism means one curve: `sizeThickness(span)` is a smoothstep from
1750
+ * `sizeSpanMin` to `sizeSpanMax`, and the thickness-derived facets are gains
1751
+ * on it — the lens (`lensSizeGainMax`), the occlusion (`sizeOcclusionGain`)
1752
+ * and the inner shadow (`sizeShadowGainMax`). The scattering was one of them
1753
+ * until W11c measured its curve to be a different one (a floor at small
1754
+ * spans, a band top past 96 — see `sizeScatterFloor`); it now rides its own,
1755
+ * from the same `sizeSpanMin`, and this band is untouched by it.
1442
1756
  *
1443
1757
  * A smoothstep rather than a straight ratio, so two surfaces of nearly the same
1444
- * size never read as differently thick, and so the gain saturates instead of
1445
- * growing without bound on a full-width platter.
1758
+ * size never read as differently thick, and so every gain saturates instead of
1759
+ * growing without bound on a full-width platter. Below `sizeSpanMin` the whole
1760
+ * law is **exactly inert**: a small control renders as it did before the law
1761
+ * existed, which is what makes the law additive rather than a global retune.
1762
+ *
1763
+ * MEASURED (W2, on the settled bed): the band is where the reference's own
1764
+ * size-dependence happens. Over a fixed checkerboard backdrop the light-standard
1765
+ * reference passes 0.244 of the backdrop's contrast at a 32 px span, 0.230 at
1766
+ * 44 px and 0.144 at 96 px, and its backdrop correlation falls 0.634 → 0.606 →
1767
+ * 0.475 across the same three — so the movement is essentially complete by 96 px
1768
+ * and has barely started at 32. See the claims doc's size-law section.
1769
+ */
1770
+ readonly sizeSpanMin: number;
1771
+ readonly sizeSpanMax: number;
1772
+ /**
1773
+ * The lens's gain on the size curve — "more pronounced lensing and refraction".
1774
+ *
1775
+ * `lensDepthPx` is `thickness × lensSizeGain(span)`, clamped to the shorter
1776
+ * *half* extent. The clamp is what keeps a small control from being all lens: a
1777
+ * 24 px-tall button cannot bend more than 12 px of backdrop however thick it is
1778
+ * authored.
1446
1779
  */
1447
- readonly lensSpanMin: number;
1448
- readonly lensSpanMax: number;
1449
1780
  readonly lensSizeGainMax: number;
1450
- /** Chain LOD per CSS px of lens depth, and how much sharper the rim samples. */
1451
- readonly lensBodyLodPerPx: number;
1452
- readonly lensRimLodBias: number;
1781
+ /**
1782
+ * The scattering gain — "a softer scattering of light". How many times wider
1783
+ * the material's body blur runs at full size.
1784
+ *
1785
+ * **The facet the settled bed identifies most directly.** Two backdrops
1786
+ * disagree in exactly the way a widening kernel predicts and an opacity change
1787
+ * does not. Over the checkerboard — all of whose structure sits at one 16 px
1788
+ * period, and whose surroundings carry the same mean as its interior — the
1789
+ * reference's retained contrast falls 41% from a 32 px span to a 96 px one while
1790
+ * its interior *level* stays put (0.607 → 0.641). Over the synthetic photo —
1791
+ * broadband, and with surroundings whose mean differs from the mask's — the
1792
+ * retained contrast barely moves between 44 px and 96 px (0.546 → 0.544) while
1793
+ * the level converges toward the neighbourhood (0.585 → 0.628). A larger alpha
1794
+ * would have moved both backdrops' contrast together and pulled both levels
1795
+ * toward the tint; a wider kernel moves exactly what moved.
1796
+ *
1797
+ * Both tiers carry it, from one function (`sizeScatterSigma`): the CSS tier
1798
+ * multiplies its `blur()` σ, and the GPU tier lerps its body sample toward the
1799
+ * chain level whose blur is that σ.
1800
+ *
1801
+ * MEASURED (W11c G1, claims §5.41), and no longer a gain on `sizeThickness`
1802
+ * alone — see `sizeScatterFloor` and `sizeScatterSpanMax` below for the curve
1803
+ * it rides. 8: the heavy component of the reference's interior sits near σ 10
1804
+ * CSS px against a body σ of 1.25, and the gain sweep has a clear minimum at
1805
+ * 8 (RMS 0.0164 against 0.0180 at 6 and 0.0191 at 10 on the probe bed).
1806
+ */
1807
+ readonly sizeScatterGainMax: number;
1808
+ /**
1809
+ * **The scattering facet's own curve** (W11c G1, claims §5.41).
1810
+ *
1811
+ * The reference's interior over structured content is two components, read
1812
+ * off the W9 probe bed across four checkerboard pitches and five spans: a
1813
+ * sharp one near σ 1.25 CSS px and a heavy one near σ 10, mixed by a share
1814
+ * that is already ≈ 0.4 at spans of 32–44 and still rising at 160 (0.52 at
1815
+ * 96, 0.64 at 128, 0.76 at 160). `sizeThickness` — zero at `sizeSpanMin` and
1816
+ * saturated at `sizeSpanMax` = 96 — can express neither the floor nor a band
1817
+ * top past 96, and moving `sizeSpanMax` would move the lens, the occlusion,
1818
+ * the inner shadow and W9's thin/thick response rows with it. So the scatter
1819
+ * mix rides its own curve:
1820
+ *
1821
+ * ```
1822
+ * kScatter = floor + (1 − floor) · smoothstep(sizeSpanMin, sizeScatterSpanMax, span) · fold
1823
+ * ```
1824
+ *
1825
+ * The floor is the material's own frost and is **not** folded under an
1826
+ * accessibility preference; the span-dependent rise is a depth effect and
1827
+ * folds like every other facet (`scatterThickness`). Fitted with `rrect-lg`
1828
+ * held out: floor 0.40 (0.0175 at 0.3, 0.0174 at 0.45), band top 256 (0.0182
1829
+ * at 224, 0.0174 at 320); the held-out cell's residual 0.0366 → 0.0174.
1830
+ */
1831
+ readonly sizeScatterFloor: number;
1832
+ readonly sizeScatterSpanMax: number;
1833
+ /**
1834
+ * The occlusion gain — "a larger size is more opaque. A smaller size is
1835
+ * clearer" (S284). The fraction of the *remaining* transparency the size law
1836
+ * closes at full size.
1837
+ *
1838
+ * Relative rather than absolute, for `increasedOcclusionLift`'s reason: a floor
1839
+ * dies silently the moment nominal passes it, and a fraction of the headroom
1840
+ * cannot. It also composes correctly with the accessibility lift — under reduced
1841
+ * transparency nominal is already near 1, so the size law has almost no headroom
1842
+ * left to close, which is exactly what the reference does there (its transmission
1843
+ * reads 0.011 at a 44 px span and 0.014 at 96 px — no size dependence, because
1844
+ * there is none left to have).
1845
+ */
1846
+ readonly sizeOcclusionGain: number;
1847
+ /**
1848
+ * The inner shadow's gain — "casts deeper, richer shadows". A multiplier on
1849
+ * `shadowDepth` at full size.
1850
+ *
1851
+ * **Coupled by construction, not fitted, and the difference is stated rather
1852
+ * than hidden.** The fixtures cannot identify it: the reference's peak darkening
1853
+ * outside its contour measures 0.0000–0.0001 on almost every calibration scene
1854
+ * and vitrea's measures the same order (C9a, `shadowFalloff`), so there is no
1855
+ * measured gap for a sweep to close, and what this renderer's `shadowDepth`
1856
+ * scales is an *inner* shadow whose contribution to the interior level is
1857
+ * degenerate with the tint's — two constants, one observable. So the direction
1858
+ * comes from Apple's sentence and the magnitude is held to what the objective is
1859
+ * flat over, with that flatness recorded. GPU tier only: the CSS tier's shadow is
1860
+ * an outer `box-shadow` the reference does not cast at all (Decision Log #32(c)).
1861
+ */
1862
+ readonly sizeShadowGainMax: number;
1863
+ /**
1864
+ * The lens's magnitude: the contour displacement in lens depths (W11c G2).
1865
+ *
1866
+ * The shader reads the body from `lensDepth × lensRefractionGain × (1 − depth)²`
1867
+ * further inside, so the band is the plate folded back from that far in. The
1868
+ * two rim-LOD constants that used to sit here (`lensBodyLodPerPx`,
1869
+ * `lensRimLodBias`) are retired: the reference's band is not sharper than its
1870
+ * interior — W11c G2 measured σ 6 → 4 → 3 → the deep value across the band —
1871
+ * and the "sharper rim" was the fold, not a finer sample.
1872
+ */
1873
+ readonly lensRefractionGain: number;
1453
1874
  /**
1454
1875
  * What each accessibility regime does to the numbers above. The multipliers
1455
1876
  * match `platform-web`'s CSS tier so the two renderers degrade the same way
@@ -1471,12 +1892,137 @@ interface MaterialProfile {
1471
1892
  * (0.62 − 0.28) / (1 − 0.28) = 0.4722, which reproduces the old floor exactly at
1472
1893
  * the old nominal. At today's nominal it reads 0.62 → 0.799.
1473
1894
  *
1474
- * Mirrored by `@vitrea/platform-web`'s `INCREASED_OCCLUSION_LIFT`, and pinned in
1895
+ * Mirrored by `@vitreajs/vitrea-web`'s `INCREASED_OCCLUSION_LIFT`, and pinned in
1475
1896
  * both directions by `packages/calibration/test/tier-coherence.test.ts`.
1476
1897
  */
1477
1898
  readonly increasedOcclusionLift: number;
1478
1899
  readonly strongBorderRim: MaterialRim;
1479
1900
  readonly reducedTintAdaptation: number;
1901
+ /**
1902
+ * The author tint's shade law (W10) — Apple's "range of tones **mapped to
1903
+ * content brightness underneath**" (S219), measured per pixel on the frozen
1904
+ * bed and the W9 probe (claims §5.36).
1905
+ *
1906
+ * The tinted material is an OPAQUE, hue-preserving shade of the seed: the seed
1907
+ * times a scalar, and the scalar is linear in the luminance the untinted
1908
+ * material shows at the same pixel — `mix(tintShadeDark, tintShadeLight, u)`,
1909
+ * clamped so a shade is never brighter than the seed. Over black content the
1910
+ * reference shows about half the seed's light; over white it shows the seed
1911
+ * itself; over a checkerboard it shows both, cell by cell, with the seed's
1912
+ * chromaticity intact to three decimals at every pitch measured. That layer
1913
+ * composites over the material at the AUTHOR's opacity (the colour's alpha) in
1914
+ * the encoded space — the half-strength cell is the 0.501 encoded-space mix
1915
+ * of its untinted and full-tinted twins, per channel — so the material's own
1916
+ * alpha is not what a tinted surface shows.
1917
+ *
1918
+ * `tintShadeStrength` is the law's provenance gate: 1 where the constants were
1919
+ * measured (the light scheme), 0 where they were not — the dark scheme
1920
+ * renders the pure seed over every backdrop it was measured on, which is
1921
+ * consistent with "a shade relative to the material's own body level" but is
1922
+ * not yet separable from "no shading in the dark scheme". The collapse (W7)
1923
+ * folds the shade out the same way, because a collapsed material IS a dark
1924
+ * body and the reference renders the pure seed there too.
1925
+ *
1926
+ * MEASURED, both scales, twelve light-standard cells across three pitches:
1927
+ * fitted on the five probe cells only (17 700 px, RMS 0.0035) and refereed by
1928
+ * every canonical tinted row. Mirrored by `@vitreajs/vitrea-web`'s
1929
+ * `TINT_SHADE`, pinned in both directions by
1930
+ * `packages/calibration/test/tier-coherence.test.ts`.
1931
+ */
1932
+ readonly tintShadeDark: number;
1933
+ readonly tintShadeLight: number;
1934
+ readonly tintShadeStrength: number;
1935
+ /**
1936
+ * **Backdrop tone adaptation (W7)** — the axis Apple's material has and this
1937
+ * one did not: over a dark enough backdrop the material stops being a lighter
1938
+ * thing in front of it and takes the backdrop's own tone.
1939
+ *
1940
+ * The mechanism is one mix, and it is deliberately the *tint colour* rather
1941
+ * than the tint alpha: `backdropToneMax` at full strength makes the tint equal
1942
+ * the sampled backdrop, so `mix(backdrop, tint, tintAlpha)` collapses to the
1943
+ * backdrop exactly and the surface is left with its rim, its inner shadow and
1944
+ * its lensing and nothing else. That is what the settled reference does —
1945
+ * `dark-solid__capsule-button__rest` is byte-identical to its own background
1946
+ * in every standard profile, at both scales, in both colour schemes.
1947
+ *
1948
+ * `backdropToneLow`/`backdropToneHigh` are the backdrop luminances (linear)
1949
+ * the two ends are reached at, crossed with a smoothstep for the same reason
1950
+ * `tintToneLow`/`High` are.
1951
+ *
1952
+ * `backdropToneSizeBias` is the size gate, and it is not decoration: the same
1953
+ * backdrop moves a small surface and a large one by very different amounts.
1954
+ * Over `dark-solid` the reference's 44 px capsule adapts completely while its
1955
+ * 96 px rrect keeps three quarters of its own appearance, measured on both
1956
+ * scales independently and agreeing to three decimals. The bias enters the
1957
+ * curve's *argument* rather than its amplitude — a thicker surface behaves as
1958
+ * though its backdrop were brighter, which is what more material between the
1959
+ * viewer and the backdrop means — because an amplitude gate cannot reproduce
1960
+ * the second dark backdrop (`impulse`) and this does.
1961
+ *
1962
+ * The axis is WITHIN a colour scheme. The scheme picks the neutral; this moves
1963
+ * the material away from that neutral toward what is actually behind it. So the
1964
+ * dark profile runs the same law with the same constants and does not
1965
+ * double-adapt: over `dark-solid` its capsule collapses onto the backdrop too,
1966
+ * and the light and dark references become the same pixels there.
1967
+ */
1968
+ readonly backdropToneMax: number;
1969
+ readonly backdropToneLow: number;
1970
+ readonly backdropToneHigh: number;
1971
+ readonly backdropToneSizeBias: number;
1972
+ /**
1973
+ * **The backdrop tone response (W9)** — the law that owns the interior MEAN,
1974
+ * where the four constants above own texture collapse and nothing else.
1975
+ *
1976
+ * The W9 probe falsified the mix-toward-backdrop form outright (claims
1977
+ * §5.33): six cells need mix strengths outside [0, 1], because the
1978
+ * reference adapts toward the material's own light/dark appearance, which
1979
+ * coincides with the backdrop's tone only over `dark-solid` — the one
1980
+ * background the mix was fitted on. What the probe measured instead is a
1981
+ * LAW: at equal encoded-space backdrop mean the reference's settled
1982
+ * interior is the same number regardless of the backdrop's structure
1983
+ * (checker vs text rows, 0.81 both at input 0.69), so the interior level is
1984
+ * a function `R(encodedMean, thickness)` and these constants are that
1985
+ * function's anchors.
1986
+ *
1987
+ * `backdropToneAnchorX` is the three solid anchors' encoded-space means —
1988
+ * measured backdrop luminances, not tuned. `backdropToneResponseThin` and
1989
+ * `...Thick` are the reference's settled interior levels at those anchors
1990
+ * for a thin surface (`sizeThickness` 0, the 32 px rrect) and a thick one
1991
+ * (saturated, the 96 px+ rrects pooled — their residual spread ±0.012 is
1992
+ * the accepted cost of `sizeSpanMax` saturation, claims §5.33). Between
1993
+ * the rows: smoothstep in thickness; between the anchors: monotone
1994
+ * (Fritsch–Carlson) interpolation in the ENCODED input, the space the
1995
+ * probe validated to RMS 0.034 with zero fitting.
1996
+ *
1997
+ * Below the dark anchor the surface has no data (the probe's grid floor
1998
+ * is `dark-solid`), and the one validation cell down there —
1999
+ * `impulse__rrect-md`, backdrop 0.0039, reference 0.4358 — reads DARKER
2000
+ * than the dark anchor's thick row, so clamping would regress it. The
2001
+ * solve's authority fades to zero from the dark anchor downward
2002
+ * (smoothstep over the anchor's own lower half, no new constant); the
2003
+ * extreme-dark region stays with the collapse constants that were fitted
2004
+ * on it.
2005
+ */
2006
+ readonly backdropToneAnchorX: readonly [number, number, number];
2007
+ readonly backdropToneResponseThin: readonly [number, number, number];
2008
+ readonly backdropToneResponseThick: readonly [number, number, number];
2009
+ /**
2010
+ * How much authority the response law has in THIS profile, 0…1 (W9).
2011
+ *
2012
+ * The anchors above are measurements of the LIGHT reference's settled
2013
+ * appearance. The dark scheme's material settles at its own levels over the
2014
+ * same backdrops (0.809 light against 0.055 dark over one checkerboard,
2015
+ * §5.8), so a dark profile running the light response surface brightens the
2016
+ * dark material toward the wrong scheme's appearance — measured on the
2017
+ * canonical dark bed as ΔE p95 0.08 → 0.58 before this constant existed.
2018
+ * The dark profiles set it to 0: the collapse (whose target is the
2019
+ * backdrop, not a scheme's appearance) still runs there, and a dark
2020
+ * response surface is follow-up work that needs the dark reference probed,
2021
+ * not an assumption.
2022
+ */
2023
+ readonly backdropToneResponseStrength: number;
2024
+ /** The outer shadow (W8) — see `MaterialOuterShadow`. */
2025
+ readonly outerShadow: MaterialOuterShadow;
1480
2026
  /**
1481
2027
  * Advisory light direction, in viewport coordinates with y pointing down: a
1482
2028
  * little left of straight overhead, which is where Apple's material reads its
@@ -1503,15 +2049,31 @@ interface MaterialProfilePatch {
1503
2049
  readonly adaptiveLuminanceLow?: number;
1504
2050
  readonly adaptiveLuminanceHigh?: number;
1505
2051
  readonly refractionScale?: Readonly<Partial<Record<RefractionQuality, number>>>;
1506
- readonly lensSpanMin?: number;
1507
- readonly lensSpanMax?: number;
2052
+ readonly sizeSpanMin?: number;
2053
+ readonly sizeSpanMax?: number;
1508
2054
  readonly lensSizeGainMax?: number;
1509
- readonly lensBodyLodPerPx?: number;
1510
- readonly lensRimLodBias?: number;
2055
+ readonly sizeScatterGainMax?: number;
2056
+ readonly sizeScatterFloor?: number;
2057
+ readonly sizeScatterSpanMax?: number;
2058
+ readonly sizeOcclusionGain?: number;
2059
+ readonly sizeShadowGainMax?: number;
2060
+ readonly lensRefractionGain?: number;
1511
2061
  readonly reducedTransparencyFrost?: number;
1512
2062
  readonly increasedOcclusionLift?: number;
1513
2063
  readonly strongBorderRim?: Readonly<Partial<MaterialRim>>;
1514
2064
  readonly reducedTintAdaptation?: number;
2065
+ readonly tintShadeDark?: number;
2066
+ readonly tintShadeLight?: number;
2067
+ readonly tintShadeStrength?: number;
2068
+ readonly backdropToneMax?: number;
2069
+ readonly backdropToneLow?: number;
2070
+ readonly backdropToneHigh?: number;
2071
+ readonly backdropToneSizeBias?: number;
2072
+ readonly backdropToneAnchorX?: readonly [number, number, number];
2073
+ readonly backdropToneResponseThin?: readonly [number, number, number];
2074
+ readonly backdropToneResponseThick?: readonly [number, number, number];
2075
+ readonly backdropToneResponseStrength?: number;
2076
+ readonly outerShadow?: Readonly<Partial<MaterialOuterShadow>>;
1515
2077
  readonly lightDirection?: readonly [number, number];
1516
2078
  readonly sweepBandRadians?: number;
1517
2079
  readonly glowRadiusCss?: number;
@@ -1734,137 +2296,6 @@ interface VideoProviderOptions {
1734
2296
  readonly generation?: number;
1735
2297
  }
1736
2298
 
1737
- /**
1738
- * Device ownership, loss teardown, and rebuild (§GPU device ownership).
1739
- *
1740
- * `platform-web` owns the *browser* half of the story — is there an adapter, get
1741
- * a device, notice when it goes away. This module owns the *resource* half: what
1742
- * has to be thrown away when a device dies, when it is safe to build again, and
1743
- * what core's capability inputs should read while that is in flight.
1744
- *
1745
- * ## The two ownership modes, and why they are not symmetric
1746
- *
1747
- * - **vitrea-owned** — this module can re-request a device by itself, so loss is
1748
- * a transient it recovers from: tear down, re-request, re-attach, rebuild.
1749
- * Every backdrop source is re-imported on the next frame because loss marks
1750
- * them all dirty, which routes recovery through the same
1751
- * one-rebuild-per-dirty-source-per-frame path a content change takes rather
1752
- * than through a special case.
1753
- * - **app-owned** — the app owns the resources that would have to be
1754
- * re-registered, so this module reports the loss, raises
1755
- * `replacementPending`, and **waits**. Groups stay demoted until
1756
- * `replaceDevice` arrives and the re-registration handshake completes. Inventing
1757
- * a device the app did not give us would break the one guarantee app-ownership
1758
- * exists for: that every texture the renderer samples came from the app's own
1759
- * device, because WebGPU has no cross-device sharing.
1760
- *
1761
- * `info.reason === "destroyed"` is our own teardown and is not recovered from —
1762
- * re-requesting there would resurrect a renderer the host just destroyed.
1763
- *
1764
- * ## Generations
1765
- *
1766
- * Every attach bumps a generation counter, and every GPU object this package
1767
- * holds is tagged with the generation it was made under. A resource from a lost
1768
- * device is unusable forever, and the failure mode is silent — bindings simply
1769
- * produce nothing — so "is this from the current device" has to be a cheap
1770
- * integer comparison somewhere. It is here.
1771
- */
1772
- /**
1773
- * Structurally identical to `vitrea`'s `WebGPUAvailability` (X2's K1
1774
- * amendment, Decision Log #21c). Declared here rather than imported because this
1775
- * package sits *below* core in the dependency graph — core reaches the renderer
1776
- * through a dynamic import, so an import back would close a cycle. A test pins
1777
- * the two unions to the same members.
1778
- */
1779
- type WebGPUAvailability = "not-requested" | "unavailable" | "available";
1780
- type DeviceOwnership = "vitrea" | "app";
1781
- interface RendererDeviceStatus {
1782
- /** Feeds core's `PlatformProbe.webgpu` unchanged. */
1783
- readonly webgpu: WebGPUAvailability;
1784
- readonly deviceHealth: "ok" | "lost";
1785
- readonly ownership: DeviceOwnership;
1786
- readonly device: GPUDevice | undefined;
1787
- /** Bumped on every attach. Tags every resource built under it. */
1788
- readonly generation: number;
1789
- /** True while an app-owned device is lost and no replacement has arrived. */
1790
- readonly replacementPending: boolean;
1791
- /** Why there is no device, when there is a reason worth reporting. */
1792
- readonly unavailableReason?: "no-adapter" | "device-request-failed" | "lost";
1793
- }
1794
- /** The capability facts a host folds into core's `PlatformProbe`. */
1795
- interface DeviceCapabilityInput {
1796
- readonly webgpu: WebGPUAvailability;
1797
- readonly deviceHealth: "ok" | "lost";
1798
- }
1799
-
1800
- /**
1801
- * The governor's knobs.
1802
- *
1803
- * §Performance envelope: "The governor degrades **within** a tier first
1804
- * (refraction resolution, adaptation cadence, edge analysis) and switches tiers
1805
- * only with long hysteresis and cooldown." Decision Log #19 adds that intra-tier
1806
- * degradation is **not a state change** — a group under `degrade-in-tier`
1807
- * pressure keeps `activeRenderer: "webgpu"` and its whole resolved state.
1808
- *
1809
- * So the split is: **the policy lives in core, the knobs live here.** This module
1810
- * exposes what can be turned and publishes a suggested ladder as *data*; it never
1811
- * decides to turn anything, never reads a clock, and never applies hysteresis.
1812
- * A governor that lived in the renderer would be a governor that could not see
1813
- * the scene it is governing.
1814
- *
1815
- * ## The three knobs, weakest cost first
1816
- *
1817
- * 1. **`fieldFamily`** — `rsupn` → `rsup`. S2 priced this at 29% of the field's
1818
- * cost for a bound that degrades from 0.170 px / 2.91° to 0.574 px / 4.26°.
1819
- * It is the *first* step because it is one uniform and one pipeline: no
1820
- * resolution change, so nothing resamples and nothing shimmers as it engages.
1821
- * **Conditional on the f32 cross-check** (Decision Log #20) — and the check
1822
- * has now been run, so the condition is met. See `FAMILY_C_CROSS_CHECK`.
1823
- * 2. **`refractionResolutionScale`** — the group's **field** targets are
1824
- * rasterised at a fraction of device resolution, and the optics and highlight
1825
- * passes filter them instead of indexing them (`passes.ts`, and the
1826
- * `fieldUpsampled` flag both shaders branch on). Quadratic saving on the pass
1827
- * that evaluates the pseudo-SDF union per pixel per member, and the most
1828
- * visible step, so it comes second.
1829
- *
1830
- * What it does **not** yet do is shrink the optics and highlight passes
1831
- * themselves: those still render at full device resolution into the plane
1832
- * canvases, because rendering them smaller means an offscreen target and a
1833
- * resolve pass rather than a change of extent. So rungs 2 and 3 deliver the
1834
- * field's quadratic saving and the cadence saving, not the full quadratic
1835
- * saving on both heavy passes. Recorded here rather than implied, because a
1836
- * ladder whose rungs are priced for savings they do not deliver is a ladder
1837
- * core's policy will walk too far down.
1838
- * 3. **`adaptationCadenceHz`** — how often analysis is reduced and read back.
1839
- * Cheapest of the three in visual terms because the values it feeds are
1840
- * already low-passed over hundreds of milliseconds; dropping the cadence
1841
- * mostly changes how quickly a scroll's new backdrop is noticed.
1842
- */
1843
- type FieldFamily = "rsupn" | "rsup";
1844
- interface GovernorKnobs {
1845
- /** Which pseudo-SDF family the field pass compiles. */
1846
- readonly fieldFamily: FieldFamily;
1847
- /**
1848
- * Device-resolution fraction the group's field targets are rasterised at,
1849
- * 0 < s <= 1. The optics and highlight passes upsample what they read. See the
1850
- * module note for what this does and does not save.
1851
- */
1852
- readonly refractionResolutionScale: number;
1853
- /** Analysis reduction + readback rate. 0 disables adaptation entirely. */
1854
- readonly adaptationCadenceHz: number;
1855
- }
1856
- interface Governor {
1857
- readonly knobs: GovernorKnobs;
1858
- /** True when `fieldFamily: "rsup"` is permitted to engage. */
1859
- readonly familyCVerified: boolean;
1860
- set(patch: Partial<GovernorKnobs>): GovernorKnobs;
1861
- /** Move to a rung of the suggested ladder. Out-of-range clamps to the ends. */
1862
- setLevel(level: number): GovernorKnobs;
1863
- reset(): GovernorKnobs;
1864
- /** Record that the cross-check passed, unlocking family C. */
1865
- recordFamilyCVerified(): void;
1866
- }
1867
-
1868
2299
  /**
1869
2300
  * What the renderer consumes each frame, and why it is declared here rather than
1870
2301
  * imported from `vitrea`.
@@ -1896,6 +2327,13 @@ interface Governor {
1896
2327
  * none in core.
1897
2328
  */
1898
2329
 
2330
+ /** Viewport-space rectangle in CSS px. Matches core's `Rect`. */
2331
+ interface Rect {
2332
+ readonly x: number;
2333
+ readonly y: number;
2334
+ readonly width: number;
2335
+ readonly height: number;
2336
+ }
1899
2337
  /** Motion-driver outputs for one surface. Every one is a value, never a time. */
1900
2338
  interface SurfaceChannels {
1901
2339
  /** `pressCompression`, 0..1. Scaled by motion's `pressCompressionScale`. */
@@ -1909,6 +2347,11 @@ interface SurfaceChannels {
1909
2347
  /** Press point in viewport CSS px. Defaults to the surface's centre. */
1910
2348
  readonly pressPoint?: readonly [number, number];
1911
2349
  }
2350
+ /** An author tint as this package takes it: a seed in linear light, and a strength. */
2351
+ interface MaterialTintInput {
2352
+ readonly color: readonly [number, number, number];
2353
+ readonly strength: number;
2354
+ }
1912
2355
  interface SurfaceInput {
1913
2356
  readonly nodeId: string;
1914
2357
  readonly family: ShapeFamily;
@@ -1917,6 +2360,17 @@ interface SurfaceInput {
1917
2360
  /** Defaults to `"apple-continuous"` — see the module note. */
1918
2361
  readonly reference?: CornerReference;
1919
2362
  readonly variant?: MaterialVariant;
2363
+ /**
2364
+ * The author's tint (core's `ResolvedMaterial.tint`), in **linear** light —
2365
+ * the host converts, because this package's whole optical model is linear and
2366
+ * the seed is about to be mixed into it.
2367
+ *
2368
+ * The strength travels per surface and reaches the fragment stage per pixel;
2369
+ * the seed colour is resolved once per group, because a group is one optics
2370
+ * pass. Core warns when a group's members ask for different seeds
2371
+ * (`tint-mixing`), and the first tinted member's colour is the one drawn.
2372
+ */
2373
+ readonly tint?: MaterialTintInput;
1920
2374
  readonly channels?: Partial<SurfaceChannels>;
1921
2375
  /**
1922
2376
  * X8 rider 2. When present this surface renders as `parentField + inset`, and
@@ -1951,6 +2405,63 @@ interface GroupRenderInput {
1951
2405
  readonly refraction: RefractionQuality;
1952
2406
  /** True where X2 resolved `analysis: "exact"`; gates adaptive tint. */
1953
2407
  readonly analysisExact: boolean;
2408
+ /**
2409
+ * The backdrop source's own average colour, linear light — what backdrop tone
2410
+ * adaptation (W7) adapts toward, and the luminance it decides from.
2411
+ *
2412
+ * A **per-source scalar rather than a per-pixel sample**, and that is why it
2413
+ * arrives from outside rather than being read off the pyramid. The host measures
2414
+ * it once from the pixels it already supplied and both tiers then read the
2415
+ * identical number: the CSS tier cannot sample per pixel at all, and a per-pixel
2416
+ * GPU adaptation beside a per-surface CSS one puts the two tiers on different
2417
+ * pictures wherever the backdrop has structure — measured on the impulse cell at
2418
+ * an interior level ratio of 79 against a gated band of 0.80…1.25. It is also
2419
+ * what the reference does: its capsule over a sparse bright grid is a *flat*
2420
+ * body, not a window onto the grid.
2421
+ *
2422
+ * Absent means the adaptation stands down for this group, rather than falling
2423
+ * back to a level nobody measured.
2424
+ */
2425
+ readonly backdropTone?: Rgb;
2426
+ /**
2427
+ * The backdrop's ENCODED-space tone level (W9, claims §5.31–§5.34): the mean
2428
+ * taken in sRGB-encoded space, decoded once — the input the reference's tone
2429
+ * response tracks, feeding the collapse band's argument and the response
2430
+ * curve. Distinct from `backdropTone` on any structured backdrop: the
2431
+ * COLOUR is the physical linear mean (what the collapse converges onto —
2432
+ * on the impulse grid the two differ 2.6×, and converging onto the encoded
2433
+ * reading was a measured ΔE p95 0.03 → 0.12 regression), while the LEVEL is
2434
+ * the encoded reading. Absent falls back to `backdropTone`'s own luminance.
2435
+ */
2436
+ readonly backdropToneLevel?: number;
2437
+ /**
2438
+ * The backdrop's LINEAR-space mean luminance, beside `backdropToneLevel`'s
2439
+ * encoded-space reading (W9, claims §5.31). The per-pixel tint-tone input
2440
+ * samples a chain that averages linearly, so the optics pass multiplies it by
2441
+ * `backdropToneLevel / backdropToneLinearLuminance` — locality is
2442
+ * preserved and the input's spatial mean matches the model exactly. Absent
2443
+ * (or equal to the tone's level) collapses the ratio to 1.
2444
+ */
2445
+ readonly backdropToneLinearLuminance?: number;
2446
+ /**
2447
+ * The material's tint and alpha for a group that samples NOTHING (W11a) — a
2448
+ * `css-backdrop` group whose blurred backdrop is a DOM proxy beneath the
2449
+ * canvas, or a `none` group over the page itself. Such a group's body is
2450
+ * not composited in the shader: the optics pass writes the material as a
2451
+ * premultiplied layer and the browser composites it over the DOM, in
2452
+ * encoded sRGB. A linear-light alpha written into that composite lands the
2453
+ * surface darker than the same material sampled on the GPU — the gap
2454
+ * `cssTintAlpha` closes for the CSS tier, at a declared reference level —
2455
+ * so the host, which owns that mapping, resolves the pair once and hands
2456
+ * the same numbers to both tiers. Linear light; the renderer folds the
2457
+ * accessibility policy over it exactly as over the profile's own. Ignored
2458
+ * wherever the group samples a backdrop; absent, the profile's pair is
2459
+ * written as it is (the golden harness, which has no host).
2460
+ */
2461
+ readonly unsampledMaterial?: {
2462
+ readonly tint: Rgb;
2463
+ readonly tintAlpha: number;
2464
+ };
1954
2465
  readonly variant?: MaterialVariant;
1955
2466
  /** Overrides the calibration-delegated union defaults. */
1956
2467
  readonly union?: GroupUnionParams;
@@ -1999,6 +2510,173 @@ interface FrameParticipantView {
1999
2510
  readonly render?: (context: FrameContextView) => void;
2000
2511
  }
2001
2512
 
2513
+ /**
2514
+ * Where a backdrop texture sits on the plane, and what that makes of a CSS px.
2515
+ *
2516
+ * The optics pass samples the backdrop through one uv transform on
2517
+ * viewport-normalised coordinates — `uv = viewport01 · scale + offset` — and
2518
+ * the pyramid converts the material's CSS-px σ into source texels once, at build.
2519
+ * Both numbers are functions of the same thing: the rectangle of the viewport the
2520
+ * texture's pixels actually occupy. This module is the one place that rectangle
2521
+ * turns into either.
2522
+ *
2523
+ * ## Placed
2524
+ *
2525
+ * A texture handed over from a DOM element — an `<img>`, a `<canvas>`, a
2526
+ * `<video>` — has a box the host measures every read phase, in CSS px relative
2527
+ * to the viewport, and that box IS the placement: a 320 px raster in the corner
2528
+ * of a 1440 px page maps the 320 px, not the page. The source's own extent is
2529
+ * still what the pyramid is built from; the placement only says where on the
2530
+ * plane its texels land, so one texel is `placement.width / sourceWidth` CSS px.
2531
+ *
2532
+ * Outside the placement the sampler clamps to the edge. A surface hanging past
2533
+ * the texture's box reads the nearest edge texel rather than nothing; the
2534
+ * unsampled layer (W11a) is not applied to partial overlap in this cut.
2535
+ *
2536
+ * ## Cover
2537
+ *
2538
+ * A source with no placement — an `ImageBitmap`, an `OffscreenCanvas`, an
2539
+ * element not in the document, or a renderer driven without a host at all —
2540
+ * keeps the original rule: the backdrop fills the viewport and the overflow is
2541
+ * cropped symmetrically. Every golden and every calibration capture was taken
2542
+ * with the texture the size of the stage, where the two rules coincide exactly;
2543
+ * the demo's reference panel is where they came apart (claims §5.47).
2544
+ */
2545
+
2546
+ /** A backdrop source's box on the plane, in CSS px relative to the viewport. */
2547
+ type BackdropPlacement = Rect;
2548
+
2549
+ /**
2550
+ * Device ownership, loss teardown, and rebuild (§GPU device ownership).
2551
+ *
2552
+ * `platform-web` owns the *browser* half of the story — is there an adapter, get
2553
+ * a device, notice when it goes away. This module owns the *resource* half: what
2554
+ * has to be thrown away when a device dies, when it is safe to build again, and
2555
+ * what core's capability inputs should read while that is in flight.
2556
+ *
2557
+ * ## The two ownership modes, and why they are not symmetric
2558
+ *
2559
+ * - **vitrea-owned** — this module can re-request a device by itself, so loss is
2560
+ * a transient it recovers from: tear down, re-request, re-attach, rebuild.
2561
+ * Every backdrop source is re-imported on the next frame because loss marks
2562
+ * them all dirty, which routes recovery through the same
2563
+ * one-rebuild-per-dirty-source-per-frame path a content change takes rather
2564
+ * than through a special case.
2565
+ * - **app-owned** — the app owns the resources that would have to be
2566
+ * re-registered, so this module reports the loss, raises
2567
+ * `replacementPending`, and **waits**. Groups stay demoted until
2568
+ * `replaceDevice` arrives and the re-registration handshake completes. Inventing
2569
+ * a device the app did not give us would break the one guarantee app-ownership
2570
+ * exists for: that every texture the renderer samples came from the app's own
2571
+ * device, because WebGPU has no cross-device sharing.
2572
+ *
2573
+ * `info.reason === "destroyed"` is our own teardown and is not recovered from —
2574
+ * re-requesting there would resurrect a renderer the host just destroyed.
2575
+ *
2576
+ * ## Generations
2577
+ *
2578
+ * Every attach bumps a generation counter, and every GPU object this package
2579
+ * holds is tagged with the generation it was made under. A resource from a lost
2580
+ * device is unusable forever, and the failure mode is silent — bindings simply
2581
+ * produce nothing — so "is this from the current device" has to be a cheap
2582
+ * integer comparison somewhere. It is here.
2583
+ */
2584
+ /**
2585
+ * Structurally identical to `vitrea`'s `WebGPUAvailability` (X2's K1
2586
+ * amendment, Decision Log #21c). Declared here rather than imported because this
2587
+ * package sits *below* core in the dependency graph — core reaches the renderer
2588
+ * through a dynamic import, so an import back would close a cycle. A test pins
2589
+ * the two unions to the same members.
2590
+ */
2591
+ type WebGPUAvailability = "not-requested" | "unavailable" | "available";
2592
+ type DeviceOwnership = "vitrea" | "app";
2593
+ interface RendererDeviceStatus {
2594
+ /** Feeds core's `PlatformProbe.webgpu` unchanged. */
2595
+ readonly webgpu: WebGPUAvailability;
2596
+ readonly deviceHealth: "ok" | "lost";
2597
+ readonly ownership: DeviceOwnership;
2598
+ readonly device: GPUDevice | undefined;
2599
+ /** Bumped on every attach. Tags every resource built under it. */
2600
+ readonly generation: number;
2601
+ /** True while an app-owned device is lost and no replacement has arrived. */
2602
+ readonly replacementPending: boolean;
2603
+ /** Why there is no device, when there is a reason worth reporting. */
2604
+ readonly unavailableReason?: "no-adapter" | "device-request-failed" | "lost";
2605
+ }
2606
+ /** The capability facts a host folds into core's `PlatformProbe`. */
2607
+ interface DeviceCapabilityInput {
2608
+ readonly webgpu: WebGPUAvailability;
2609
+ readonly deviceHealth: "ok" | "lost";
2610
+ }
2611
+
2612
+ /**
2613
+ * The governor's knobs.
2614
+ *
2615
+ * §Performance envelope: "The governor degrades **within** a tier first
2616
+ * (refraction resolution, adaptation cadence, edge analysis) and switches tiers
2617
+ * only with long hysteresis and cooldown." Decision Log #19 adds that intra-tier
2618
+ * degradation is **not a state change** — a group under `degrade-in-tier`
2619
+ * pressure keeps `activeRenderer: "webgpu"` and its whole resolved state.
2620
+ *
2621
+ * So the split is: **the policy lives in core, the knobs live here.** This module
2622
+ * exposes what can be turned and publishes a suggested ladder as *data*; it never
2623
+ * decides to turn anything, never reads a clock, and never applies hysteresis.
2624
+ * A governor that lived in the renderer would be a governor that could not see
2625
+ * the scene it is governing.
2626
+ *
2627
+ * ## The three knobs, weakest cost first
2628
+ *
2629
+ * 1. **`fieldFamily`** — `rsupn` → `rsup`. S2 priced this at 29% of the field's
2630
+ * cost for a bound that degrades from 0.170 px / 2.91° to 0.574 px / 4.26°.
2631
+ * It is the *first* step because it is one uniform and one pipeline: no
2632
+ * resolution change, so nothing resamples and nothing shimmers as it engages.
2633
+ * **Conditional on the f32 cross-check** (Decision Log #20) — and the check
2634
+ * has now been run, so the condition is met. See `FAMILY_C_CROSS_CHECK`.
2635
+ * 2. **`refractionResolutionScale`** — the group's **field** targets are
2636
+ * rasterised at a fraction of device resolution, and the optics and highlight
2637
+ * passes filter them instead of indexing them (`passes.ts`, and the
2638
+ * `fieldUpsampled` flag both shaders branch on). Quadratic saving on the pass
2639
+ * that evaluates the pseudo-SDF union per pixel per member, and the most
2640
+ * visible step, so it comes second.
2641
+ *
2642
+ * What it does **not** yet do is shrink the optics and highlight passes
2643
+ * themselves: those still render at full device resolution into the plane
2644
+ * canvases, because rendering them smaller means an offscreen target and a
2645
+ * resolve pass rather than a change of extent. So rungs 2 and 3 deliver the
2646
+ * field's quadratic saving and the cadence saving, not the full quadratic
2647
+ * saving on both heavy passes. Recorded here rather than implied, because a
2648
+ * ladder whose rungs are priced for savings they do not deliver is a ladder
2649
+ * core's policy will walk too far down.
2650
+ * 3. **`adaptationCadenceHz`** — how often analysis is reduced and read back.
2651
+ * Cheapest of the three in visual terms because the values it feeds are
2652
+ * already low-passed over hundreds of milliseconds; dropping the cadence
2653
+ * mostly changes how quickly a scroll's new backdrop is noticed.
2654
+ */
2655
+ type FieldFamily = "rsupn" | "rsup";
2656
+ interface GovernorKnobs {
2657
+ /** Which pseudo-SDF family the field pass compiles. */
2658
+ readonly fieldFamily: FieldFamily;
2659
+ /**
2660
+ * Device-resolution fraction the group's field targets are rasterised at,
2661
+ * 0 < s <= 1. The optics and highlight passes upsample what they read. See the
2662
+ * module note for what this does and does not save.
2663
+ */
2664
+ readonly refractionResolutionScale: number;
2665
+ /** Analysis reduction + readback rate. 0 disables adaptation entirely. */
2666
+ readonly adaptationCadenceHz: number;
2667
+ }
2668
+ interface Governor {
2669
+ readonly knobs: GovernorKnobs;
2670
+ /** True when `fieldFamily: "rsup"` is permitted to engage. */
2671
+ readonly familyCVerified: boolean;
2672
+ set(patch: Partial<GovernorKnobs>): GovernorKnobs;
2673
+ /** Move to a rung of the suggested ladder. Out-of-range clamps to the ends. */
2674
+ setLevel(level: number): GovernorKnobs;
2675
+ reset(): GovernorKnobs;
2676
+ /** Record that the cross-check passed, unlocking family C. */
2677
+ recordFamilyCVerified(): void;
2678
+ }
2679
+
2002
2680
  /**
2003
2681
  * Pass-by-pass timing, for the benchmark §Performance envelope pins the ~2 ms
2004
2682
  * hypothesis to.
@@ -2154,6 +2832,28 @@ interface PyramidResources {
2154
2832
  readonly sizeEpoch: number;
2155
2833
  /** The dirty epoch the last successful rebuild satisfied. */
2156
2834
  readonly builtEpoch: number;
2835
+ /**
2836
+ * The body blur's σ in **level-0 texels** — the CSS-px σ the material asked
2837
+ * for, through the same cover fit and plan downscale the build applied.
2838
+ *
2839
+ * Published because the conversion is only knowable here: it needs the frame's
2840
+ * real extent, which is exactly why `bodySigmaCss` arrives in CSS px. The size
2841
+ * law's scattering facet consumes it (W2) — the optics pass widens the body blur
2842
+ * per surface by sampling the chain, and it cannot find the level to measure
2843
+ * from without knowing where the body already sits.
2844
+ */
2845
+ readonly bodySigmaTexels: number;
2846
+ /**
2847
+ * Source texels per CSS px the build converted `bodySigmaCss` with — the placed
2848
+ * density where the source had a placement, the cover ratio where it did not
2849
+ * (`backdrop-fit.ts`) — and the source extent it was computed from. Recorded so
2850
+ * a later request can tell whether the body it would produce differs from the
2851
+ * one on the chain: a static image never re-dirties, so a placement whose SIZE
2852
+ * moved is the only signal that the σ in texels has gone stale.
2853
+ */
2854
+ readonly texelsPerCss: number;
2855
+ readonly sourceWidth: number;
2856
+ readonly sourceHeight: number;
2157
2857
  }
2158
2858
  interface PyramidInstrumentation {
2159
2859
  /** Successful rebuilds since the store was created. */
@@ -2184,6 +2884,11 @@ interface PyramidBuildRequest {
2184
2884
  */
2185
2885
  readonly bodySigmaCss: number;
2186
2886
  readonly viewportCss: readonly [number, number];
2887
+ /**
2888
+ * Where the source sits on the plane, in CSS px relative to the viewport, if
2889
+ * the host measured one. Absent, the source is cover-fit to the viewport.
2890
+ */
2891
+ readonly placement?: BackdropPlacement;
2187
2892
  }
2188
2893
  type PyramidBuildOutcome = {
2189
2894
  readonly status: "built";
@@ -2368,6 +3073,16 @@ interface GlassRenderer {
2368
3073
  registerBackdrop(provider: BackdropProvider): void;
2369
3074
  unregisterBackdrop(sourceId: string): void;
2370
3075
  backdrop(sourceId: string): BackdropProvider | undefined;
3076
+ /**
3077
+ * Where a source's pixels sit on the plane, in CSS px relative to the viewport
3078
+ * (`backdrop-fit.ts`). Set every read phase by a host that measured the source
3079
+ * element's box; `undefined` (or never set) is the cover fit, the rule every
3080
+ * golden and calibration capture was taken under. Accepted before the provider
3081
+ * is registered and kept across device generations; cleared by
3082
+ * `unregisterBackdrop`.
3083
+ */
3084
+ setBackdropPlacement(sourceId: string, placement: BackdropPlacement | undefined): void;
3085
+ backdropPlacement(sourceId: string): BackdropPlacement | undefined;
2371
3086
  setViewport(viewport: ViewportState): void;
2372
3087
  readonly viewport: ViewportState;
2373
3088
  setGroup(input: GroupRenderInput): void;
@@ -2433,7 +3148,7 @@ declare function loadWebGPURenderer(): Promise<GlassRenderer>;
2433
3148
  *
2434
3149
  * Pure and passive: no DOM, no Node built-ins (X4), no timers and no clocks.
2435
3150
  * Every probe result, media-query answer and layout rect arrives as plain data;
2436
- * @vitrea/platform-web owns the browser and drives the frames.
3151
+ * @vitreajs/vitrea-web owns the browser and drives the frames.
2437
3152
  *
2438
3153
  * The modules, roughly in dependency order:
2439
3154
  *
@@ -2463,4 +3178,4 @@ declare const VITREA_CONTRACTS: {
2463
3178
  declare const RENDERER_TIERS: readonly ["webgpu", "css"];
2464
3179
  type RendererTier = (typeof RENDERER_TIERS)[number];
2465
3180
 
2466
- export { ACCESSIBILITY_BEHAVIOR_TABLE, ACCESSIBILITY_FLAGS, ACCESSIBILITY_PRECEDENCE, type AccessibilityConsequences, type AccessibilityFlag, type AccessibilityOverride, type AccessibilityOverrides, type ActiveRenderer, type AnalysisQuality, type BackdropEstimatorProvider, type BackdropHint, type BackdropHintRequest, type BackdropProvider, type BackdropRebuildRequest, type BackdropResolutionPolicy, type BackdropSourceDescriptor, type BackdropSourceRecord, type BackdropTone, type CapabilityInputs, type ConfiguredSource, type CopyProviderOptions, type CornerProfile, type CornerRadii, DEFAULT_BACKDROP_RESOLUTION, DEFAULT_CLEAR_DIMMING, DEFAULT_GROUP_SAMPLING, DEMOTION_REASONS, DEMOTION_RECOVERY, DIAGNOSTIC_CODES, type DemotionReason, type DescriptorPatch, type Diagnostic, type DiagnosticCode, type DiagnosticSeverity, type DiagnosticSink, type DiagnosticsChannel, type DiagnosticsChannelOptions, type DimmingPolicy, type DomBackdropSource, FOREGROUND_MODES, FRAME_PHASES, type ForegroundAdaptation, type ForegroundMode, type ForegroundResolutionOptions, type FrameContext, type FrameInfo, type FrameParticipant, type FramePhase, type FrameReport, type FrameScheduler, type FrameSchedulerOptions, GLASS_PLANES, GOVERNOR_PRESSURES, type GlassGroupDescriptor, type GlassGroupRecord, type GlassGroupState, type GlassNodeDescriptor, type GlassNodeRecord, type GlassPlane, type GlassRenderer, type GlassScene, GlassSceneError, type GlassSceneErrorCode, type GlassSceneOptions, type GovernorPressure, type GroupHealth, type GroupSamplingGeometry, type GroupStateChange, HINT_AVAILABILITIES, type HintAvailability, type InteractionState, MATERIAL_VARIANTS$1 as MATERIAL_VARIANTS, type MaterialProfile$1 as MaterialProfile, type MaterialRequest, type MaterialVariant$1 as MaterialVariant, type MotionChannel, type MotionDriverKind, NOMINAL_ACCESSIBILITY_POLICY, OVERRIDABLE_ACCESSIBILITY_FLAGS, type OverridableAccessibilityFlag, type PlaneOverlap, type PlatformProbe, type ProxyOverlap, RENDERER_TIERS, type RecoveryContract, type RecoveryTrigger, type Rect, type RefractionQuality$1 as RefractionQuality, type RendererTier, type ResolvedAccessibilityPolicy, type ResolvedBackdropHint, type ResolvedForegroundAdaptation, type ResolvedGroup, type ResolvedMaterial, type ResolvedMaterialPolicy, type ResolvedMotionPolicy, type ResolvedNode, SAMPLED_ASYNC_DEFAULTS, SAMPLED_ASYNC_RATE_LIMITS, type SamplingBackend, type SceneResolution, type ShapeChannels, type ShapeFamily, type SourceProbe, type StateChange, type SystemAccessibilityPreferences, type TextureBackdropSource, VITREA_CONTRACTS, type VariantMixingCheck, type Vec2, type VideoProviderOptions, WEBGPU_AVAILABILITIES, type WebGPUAvailability$1 as WebGPUAvailability, type WebGPURendererModule, type ZSlot, checkVariantMixing, classifyStateChange, compareZSlot, createDiagnosticsChannel, createFrameScheduler, createGlassScene, defaultForegroundAdaptation, inflateRect, isHealthy, loadWebGPURenderer, loadWebGPURendererModule, rectsOverlap, resolveAccessibilityPolicy, resolveBackdropHint, resolveForegroundAdaptation, resolveGlassGroupState, resolveMaterial, unionRect };
3181
+ export { ACCESSIBILITY_BEHAVIOR_TABLE, ACCESSIBILITY_FLAGS, ACCESSIBILITY_PRECEDENCE, type AccessibilityConsequences, type AccessibilityFlag, type AccessibilityOverride, type AccessibilityOverrides, type ActiveRenderer, type AnalysisQuality, type BackdropEstimatorProvider, type BackdropHint, type BackdropHintRequest, type BackdropProvider, type BackdropRebuildRequest, type BackdropResolutionPolicy, type BackdropSourceDescriptor, type BackdropSourceRecord, type BackdropTone, type CapabilityInputs, type ConcentricParent, type ConfiguredSource, type CopyProviderOptions, type CornerProfile, type CornerRadii, type CornerReference, DEFAULT_BACKDROP_RESOLUTION, DEFAULT_CLEAR_DIMMING, DEFAULT_GROUP_SAMPLING, DEMOTION_REASONS, DEMOTION_RECOVERY, DIAGNOSTIC_CODES, type DemotionReason, type DescriptorPatch, type Diagnostic, type DiagnosticCode, type DiagnosticSeverity, type DiagnosticSink, type DiagnosticsChannel, type DiagnosticsChannelOptions, type DimmingPolicy, type DomBackdropSource, FOREGROUND_MODES, FRAME_PHASES, type ForegroundAdaptation, type ForegroundMode, type ForegroundResolutionOptions, type FrameContext, type FrameInfo, type FrameParticipant, type FramePhase, type FrameReport, type FrameScheduler, type FrameSchedulerOptions, GLASS_PLANES, GOVERNOR_PRESSURES, type GlassGroupDescriptor, type GlassGroupRecord, type GlassGroupState, type GlassNodeDescriptor, type GlassNodeRecord, type GlassPlane, type GlassRenderer, type GlassScene, GlassSceneError, type GlassSceneErrorCode, type GlassSceneOptions, type GlassTint, type GovernorPressure, type GroupHealth, type GroupSamplingGeometry, type GroupStateChange, HINT_AVAILABILITIES, type HintAvailability, type InteractionState, MATERIAL_VARIANTS$1 as MATERIAL_VARIANTS, type MaterialProfile$1 as MaterialProfile, type MaterialRequest, type MaterialVariant$1 as MaterialVariant, type MotionChannel, type MotionDriverKind, NOMINAL_ACCESSIBILITY_POLICY, OVERRIDABLE_ACCESSIBILITY_FLAGS, type OverridableAccessibilityFlag, type PlaneOverlap, type PlatformProbe, type ProxyOverlap, RENDERER_TIERS, type RecoveryContract, type RecoveryTrigger, type Rect$1 as Rect, type RefractionQuality, type RendererTier, type ResolvedAccessibilityPolicy, type ResolvedBackdropHint, type ResolvedForegroundAdaptation, type ResolvedGroup, type ResolvedMaterial, type ResolvedMaterialPolicy, type ResolvedMotionPolicy, type ResolvedNode, SAMPLED_ASYNC_DEFAULTS, SAMPLED_ASYNC_RATE_LIMITS, type SamplingBackend, type SceneResolution, type ShapeChannels, type ShapeFamily, type SourceProbe, type StateChange, type SystemAccessibilityPreferences, type TextureBackdropSource, type TintColor, type TintMixingCheck, VITREA_CONTRACTS, type VariantMixingCheck, type Vec2, type VideoProviderOptions, WEBGPU_AVAILABILITIES, type WebGPUAvailability$1 as WebGPUAvailability, type WebGPURendererModule, type ZSlot, checkTintMixing, checkVariantMixing, classifyStateChange, clipRect, compareZSlot, createDiagnosticsChannel, createFrameScheduler, createGlassScene, defaultForegroundAdaptation, glassTint, inflateRect, isHealthy, loadWebGPURenderer, loadWebGPURendererModule, rectsOverlap, resolveAccessibilityPolicy, resolveBackdropHint, resolveForegroundAdaptation, resolveGlassGroupState, resolveMaterial, unionRect };