@inventure71/paddockjs 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +92 -17
  4. package/docs/architecture.md +26 -16
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +7 -5
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +121 -33
  9. package/docs/data_model.md +1 -1
  10. package/docs/index.md +10 -0
  11. package/docs/learnings.md +6 -6
  12. package/docs/rules.md +39 -9
  13. package/docs/sense_contract.md +59 -0
  14. package/docs/system_specs.md +76 -20
  15. package/docs/training.md +27 -20
  16. package/package.json +1 -1
  17. package/src/api/PaddockSimulatorController.js +75 -36
  18. package/src/app/BrowserExpertAdapter.js +9 -300
  19. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  20. package/src/app/F1SimulatorApp.js +190 -68
  21. package/src/app/banners/raceDataBannerController.js +71 -36
  22. package/src/app/banners/stewardMessageController.js +5 -4
  23. package/src/app/camera/cameraConstants.js +2 -0
  24. package/src/app/camera/cameraController.js +86 -29
  25. package/src/app/camera/cameraGeometry.js +12 -1
  26. package/src/app/domBindings.js +89 -9
  27. package/src/app/layoutScheduler.js +27 -0
  28. package/src/app/layoutSupport.js +165 -0
  29. package/src/app/raceOverlayClearanceSupport.js +158 -0
  30. package/src/app/readouts/carOverviewRenderer.js +16 -8
  31. package/src/app/readouts/controlStateRenderer.js +1 -1
  32. package/src/app/readouts/raceStatusRenderer.js +30 -21
  33. package/src/app/readouts/readoutFormatters.js +2 -0
  34. package/src/app/readouts/telemetryRenderer.js +17 -8
  35. package/src/app/readouts/timingTowerRenderer.js +62 -10
  36. package/src/app/runtime/runtimeVisibility.js +66 -5
  37. package/src/config/cssValues.js +16 -0
  38. package/src/config/defaultOptions.js +51 -38
  39. package/src/config/restartOptions.js +34 -0
  40. package/src/config/themeOptions.js +825 -0
  41. package/src/config/timingGapMode.js +13 -0
  42. package/src/config/urlOptions.js +25 -0
  43. package/src/data/championship.js +14 -9
  44. package/src/environment/controllerLoop.js +34 -5
  45. package/src/environment/episode.js +14 -2
  46. package/src/environment/index.d.ts +17 -4
  47. package/src/environment/observationVector.js +200 -0
  48. package/src/environment/observations.js +58 -285
  49. package/src/environment/options.js +1 -1
  50. package/src/environment/recorder.js +13 -6
  51. package/src/environment/rewards.js +13 -2
  52. package/src/environment/runtime.js +65 -25
  53. package/src/environment/scenarios.js +22 -3
  54. package/src/environment/sensors/index.js +3 -3
  55. package/src/environment/sensors/opponentRadar.js +13 -3
  56. package/src/environment/sensors/sensorTargets.js +8 -2
  57. package/src/environment/sensors/surfaceRays.js +84 -12
  58. package/src/environment/sensors/trackRays.js +173 -15
  59. package/src/environment/specs.js +78 -40
  60. package/src/index.d.ts +112 -9
  61. package/src/index.js +26 -33
  62. package/src/simulation/driver/driverController.js +1 -1
  63. package/src/simulation/driver/edgeRecovery.js +1 -1
  64. package/src/simulation/driver/racingControls.js +2 -2
  65. package/src/simulation/driver/rejoinControls.js +2 -2
  66. package/src/simulation/pit/pitFlow.js +5 -0
  67. package/src/simulation/pit/pitKinematics.js +24 -0
  68. package/src/simulation/pit/pitQueue.js +5 -0
  69. package/src/simulation/pit/pitRouteMovement.js +7 -0
  70. package/src/simulation/pit/pitRouting.js +2 -4
  71. package/src/simulation/pit/pitService.js +2 -0
  72. package/src/simulation/pit/pitTireService.js +2 -0
  73. package/src/simulation/race/raceLifecycle.js +2 -6
  74. package/src/simulation/race/raceProgress.js +6 -1
  75. package/src/simulation/race/raceSetup.js +2 -6
  76. package/src/simulation/race/raceStep.js +2 -2
  77. package/src/simulation/race/redFlag.js +3 -3
  78. package/src/simulation/race/safetyCar.js +10 -4
  79. package/src/simulation/race/stalledDnf.js +8 -17
  80. package/src/simulation/raceSimulation.js +1 -0
  81. package/src/simulation/rules/rulesReview.js +3 -0
  82. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  83. package/src/simulation/timing/lapTelemetry.js +28 -9
  84. package/src/simulation/track/pitLaneLayout.js +7 -3
  85. package/src/simulation/track/spatialQueries.js +5 -28
  86. package/src/simulation/track/trackQueryGrid.js +94 -0
  87. package/src/simulation/track/trackQueryIndex.js +193 -434
  88. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  89. package/src/simulation/track/trackQueryScratch.js +46 -0
  90. package/src/simulation/track/trackQueryStats.js +28 -0
  91. package/src/simulation/track/trackStatePolicy.js +0 -1
  92. package/src/simulation/vehicle/contactResolution.js +67 -2
  93. package/src/simulation/vehicle/runoffResponse.js +11 -12
  94. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  95. package/src/simulation/vehicle/vehiclePhysics.js +5 -4
  96. package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
  97. package/src/simulation/vehicle/vehicleState.js +36 -2
  98. package/src/simulation/vehicle/wheelSurface.js +3 -1
  99. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  100. package/src/styles.css +1185 -136
  101. package/src/ui/bannerTemplates.js +38 -28
  102. package/src/ui/cameraControlsTemplate.js +16 -5
  103. package/src/ui/carOverviewTemplate.js +12 -5
  104. package/src/ui/raceCanvasTemplate.js +31 -8
  105. package/src/ui/raceControlsTemplate.js +27 -10
  106. package/src/ui/shellTemplate.js +3 -1
  107. package/src/ui/telemetryDrawerTemplate.js +23 -10
  108. package/src/ui/telemetryTemplates.js +58 -32
  109. package/src/ui/templateUtils.js +48 -0
  110. package/src/ui/timingTowerTemplate.js +29 -12
@@ -0,0 +1,72 @@
1
+ # Component Inventory
2
+
3
+ This inventory maps package-owned UI surfaces to their shared bases, owner modules, runtime wiring, and intentional differences. Keep it synchronized with `src/ui/*Template.js`, `src/app/domBindings.js`, `src/app/readouts/*`, `src/app/banners/*`, `src/app/layoutSupport.js`, `src/app/raceOverlayClearanceSupport.js`, and `src/api/PaddockSimulatorController.js`.
4
+
5
+ ## Shared Ownership Rules
6
+
7
+ - `src/ui/templateUtils.js` owns generic generated-markup helpers: escaping, loading overlays, unsupported-size overlays, and other repeated inert markup primitives.
8
+ - `src/api/PaddockSimulatorController.js` owns composable root mounting, root CSS variables, layout-support installation, and the composite query surface used by `F1SimulatorApp`.
9
+ - `src/app/domBindings.js` is the only runtime selector map for package-generated DOM. Renderers and controllers consume cached nodes from that map instead of rediscovering common readouts. When one simulator can legitimately own multiple copies of a readout surface, the binding should expose plural collections and the focused renderer should fan out updates.
10
+ - `src/app/layoutSupport.js` owns component support-envelope checks and `data-paddock-size-unsupported` state. It should stay generic and component-id driven.
11
+ - `src/app/layoutScheduler.js` owns RAF-backed layout sync coalescing and cleanup cancellation for component measurement helpers.
12
+ - `src/app/raceOverlayClearanceSupport.js` owns the measured lower-third vs embedded timing-tower clearance path. It is intentionally limited to race-canvas panels with timing towers, caches static tower/banner/toggle nodes per panel, and re-queries only dynamic timing rows during syncs.
13
+ - `src/config/themeOptions.js` owns theme token, component slot, component selector, and selected-team variable application. Templates expose `data-paddock-component`; runtime applies variables.
14
+
15
+ ## Package Components
16
+
17
+ | Component or variant | Public mount or composition | Owner module | Shared base | Runtime owner | Intentional differences |
18
+ | --- | --- | --- | --- | --- | --- |
19
+ | All-in-one shell | `mountF1Simulator()` | `src/ui/shellTemplate.js` | Composes race controls, optional external camera controls, timing tower, race canvas, telemetry stack, shell unsupported overlay | `F1SimulatorApp` | Owns `data-f1-simulator-shell`, workspace/grid classes, layout preset classes, and the package root styling scope. Not individually mounted by composable hosts. |
20
+ | Race controls | `mountRaceControls()` and all-in-one shell | `src/ui/raceControlsTemplate.js` | Component surface frame helper, `createSafetyCarControlMarkup({ compact: true })`, loading overlay, unsupported overlay | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Includes title, kicker, safe backlink, safety-car button, and restart. The compact safety button has no standalone component wrapper. |
21
+ | Safety-car control | `mountSafetyCarControl()` and drawer/race-controls compact embed | `src/ui/raceControlsTemplate.js` | Component surface frame helper for standalone, same `data-safety-car` button and control-state renderer | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Standalone variant gets `data-paddock-component="safety-car-control"` and unsupported-size overlay. Compact variants are plain subcontrols inside race controls or drawer toolbar. |
22
+ | Camera controls | `mountCameraControls()`, all-in-one external controls, embedded race-canvas controls, drawer toolbar | `src/ui/cameraControlsTemplate.js` | Component surface frame helper, shared `data-camera-mode`, zoom, banner mute, optional speed button, loading overlay, unsupported overlay | `controlStateRenderer.js`, `CameraController`, `F1SimulatorApp.bindControls()` | External variant adds `camera-controls--external`; embedded variant omits that class. Driver camera and simulation-speed buttons are option gated. |
23
+ | Timing tower | `mountTimingTower()`, all-in-one shell, embedded race canvas, drawer race canvas | `src/ui/timingTowerTemplate.js` | Component surface frame helper, race-control status banner, loading overlay, unsupported overlay, shared timing gap controls | `timingTowerRenderer.js`, `raceStatusRenderer.js`, `controlStateRenderer.js`, `F1SimulatorApp.observeLayoutResize()` | Embedded variants receive generated instance ids for `aria-controls`. `domBindings.js` exposes plural timing lists/header/status nodes and focused renderers fan out rows, lap counters, status banners, gap controls, and resize observation across every mounted tower owned by the same simulator. Header gap toggle may be hidden while controller APIs remain active. Width/proportions are package-owned and must not be tuned by host CSS. |
24
+ | Race-control status banner | Timing tower internal component | `src/ui/raceControlStatusBanner.js` | Timing tower status slot | `raceStatusRenderer.js` | Internal only. It has no public mount because it is a timing-tower status affordance, not a standalone component. |
25
+ | Race canvas | `mountRaceCanvas()` and shell/drawer templates | `src/ui/raceCanvasTemplate.js` | Component surface frame helper, steward message, optional embedded camera controls, optional race-data panel, optional sector banner, loading overlay, unsupported overlay | `F1SimulatorApp`, `CameraController`, Pixi renderers, `raceStatusRenderer.js`, `stewardMessageController.js`, `raceDataBannerController.js` | Base canvas can be canvas-only; options add timing tower, race-data lower-third, sector banner, FPS, physics indicator, and responsive narrow timing reveal. Race canvas is required before runtime start. |
26
+ | Embedded timing race canvas | `mountRaceCanvas(root, { includeTimingTower: true })`, drawer template | `src/ui/raceCanvasTemplate.js` | Timing tower template plus timing reveal button and narrow responsive classes | `F1SimulatorApp.syncTimingPanelDisclosureState()`, `installRaceOverlayClearanceSupport()` | Generates unique timing-panel ids. Narrow mode makes the tower a left reveal panel and measures banner clearance only when needed. |
27
+ | Steward message | Race canvas internal component | `src/ui/bannerTemplates.js` | Shared banner markup conventions | `stewardMessageController.js` | Internal top race-control message for warnings/penalties. No public mount because it depends on race-canvas layering. |
28
+ | Race-data panel | `mountRaceDataPanel()`, race canvas lower-third, drawer lower-third | `src/ui/bannerTemplates.js` | Component surface frame helper, selected driver readouts, optional race-data telemetry detail, loading overlay, unsupported overlay | `domBindings.js` grouped panel bindings, `raceDataBannerController.js`, `telemetryRenderer.js` for sector detail | Standalone variant adds `race-data-panel--standalone`. Embedded variants are clipped/layered by race canvas. Project and radio modes share the same panel; runtime swaps content/classes across every mounted panel owned by the simulator. |
29
+ | Telemetry sector banner | `mountTelemetrySectorBanner()` and optional race-canvas embed | `src/ui/bannerTemplates.js` | Component surface frame helper, sector-bar markup, loading overlay, unsupported overlay | `telemetryRenderer.js` | Independent broadcast lower-third style sector surface. It is separate from race-data telemetry detail and should only render when explicitly mounted or embedded. |
30
+ | Telemetry stack | `mountTelemetryPanel()` and drawer content | `src/ui/telemetryTemplates.js` | Component surface frame helper, telemetry core, sector progress, lap times, sector times, optional car/driver overview, unsupported overlay | `telemetryRenderer.js`, `carOverviewRenderer.js` when overview is included | The stack owns scrolling when constrained. It may include overview by default, but drawer content intentionally disables overview. |
31
+ | Telemetry core | `mountTelemetryCore()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Scalar selected-car telemetry only. It shares selected-code/name readouts with sector banner. |
32
+ | Telemetry sectors | `mountTelemetrySectors()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, sector-bar markup, loading overlay, unsupported overlay | `telemetryRenderer.js` | Sector progress bars only. It shares sector bar selectors with race-data detail and sector banner. |
33
+ | Telemetry lap times | `mountTelemetryLapTimes()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Current, last, and best lap table only. |
34
+ | Telemetry sector times | `mountTelemetrySectorTimes()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Last/best sector table only. |
35
+ | Car/driver overview | `mountCarDriverOverview()` and optional telemetry stack embed | `src/ui/carOverviewTemplate.js` | Component surface frame helper, overview field slots, loading overlay, unsupported overlay | `domBindings.js` cached field bindings, `carOverviewRenderer.js` | Standalone and stack-embedded variants share identical markup. Runtime toggles vehicle vs driver fields without remounting and updates cached label/value bindings instead of querying each field during render. |
36
+ | Race telemetry drawer | `mountRaceTelemetryDrawer()` and complete preview template | `src/ui/telemetryDrawerTemplate.js` | Component surface frame helper, camera controls, compact safety-car control, race canvas with timing/data panel, telemetry stack, unsupported overlay | `F1SimulatorApp.setTelemetryDrawerOpen()`, `controlStateRenderer.js`, normal race/telemetry renderers | Higher-level mixed template. It intentionally owns toolbar placement, drawer ids, open/closed inert state, and responsive narrow drawer behavior. |
37
+ | Loading overlay | All startup-capable public surfaces except internal-only steward/status | `src/ui/templateUtils.js` | `createLoadingMarkup(label)` | `F1SimulatorApp.completeComponentLoading()` | Removed after startup. Component labels remain surface-specific for accessibility. |
38
+ | Unsupported-size overlay | Public mount roots, shell, drawer, race/data/telemetry surfaces | `src/ui/templateUtils.js` | `createUnsupportedSizeMarkup(label)` | `installLayoutSupport()` | Display is driven by measured support state. Minimum size thresholds remain centralized in `layoutSupport.js`. |
39
+
40
+ ## Local Preview Variants
41
+
42
+ | Route or section | Package surfaces exercised | Owner in preview | Intentional difference |
43
+ | --- | --- | --- | --- |
44
+ | Templates complete broadcast | `createPaddockSimulator()` plus `mountRaceTelemetryDrawer()` | `local-preview/src/main.js`, `local-preview/src/showcaseCatalog.js` | Demonstrates the highest-level mixed workbench with race, timing, lower-third, top controls, and drawer in one package-owned template. |
45
+ | Templates dashboard, timing overlay, compact race, full dashboard | `mountF1Simulator()` with presets | `local-preview/src/main.js` | Demonstrates all-in-one shell presets and preset-first option override behavior. |
46
+ | Templates banner option | `mountRaceCanvas()` plus external `mountCameraControls()` | `local-preview/src/main.js` | Demonstrates a composable race window that owns timing tower and race-data lower-third while camera controls remain separate. |
47
+ | Components embedded window | `mountRaceCanvas()` with timing tower, data panel, sector banner plus external camera controls | `local-preview/src/main.js` | Exercises optional race-canvas embeds without the drawer wrapper. |
48
+ | Components separate pieces | Every public composable mount | `local-preview/src/main.js` `COMPONENT_PIECE_MOUNTS` | Proves separate host roots still receive package CSS variables, loading state, layout support, and shared runtime updates. The preview inventory table is the single owner of the separate-piece root id to mount-helper mapping; the telemetry drawer remains separate because it is a mixed higher-level template with its own simulator. |
49
+ | Customization route | Race canvas, overview, race-data panel, controls, timing tower, telemetry core | `local-preview/src/main.js` | Exercises theme packages, selected-team component theming, component overrides, and standalone vs embedded component theming. |
50
+ | API route | Race telemetry drawer | `local-preview/src/main.js` | Exercises controller methods and snapshot readout beside the package-owned workbench. |
51
+ | Behavior route | Multiple race-canvas variants | `local-preview/src/main.js` | Exercises timing expand/scroll, embedded camera controls, sector banner, finish panel, and deterministic behavior fixtures. |
52
+ | Stewarding route | Race canvas with race-data panel and penalty UI options | `local-preview/src/main.js` | Exercises steward message, timing penalty badges, and penalty controller APIs. |
53
+ | Playable route | Race telemetry drawer in browser expert mode | `local-preview/src/main.js`, `local-preview/src/playableKeyboardController.js` | Host-owned keyboard controller drives package-owned expert runtime; package UI remains the same drawer/canvas/timing/telemetry surfaces. |
54
+ | Policy Runner route | All-in-one browser expert mount | `local-preview/src/main.js`, `local-preview/src/policyRunner/*` | Host-owned model/controller panel drives package expert APIs and model-sense visualization without changing package component markup. |
55
+
56
+ ## Duplication Map
57
+
58
+ | Pattern | Current shared owner | Remaining duplication | Rationale or next action |
59
+ | --- | --- | --- | --- |
60
+ | Component surface frames | `templateUtils.js` | The all-in-one shell and internal steward message keep bespoke wrappers | Public component surfaces, including race controls, standalone safety-car control, timing tower, race canvas, race telemetry drawer, telemetry stack, race-data panel, telemetry sector banner, detached telemetry modules, car/driver overview, and camera controls now share the frame helper. The shell is a package root with `data-f1-simulator-shell`; steward message is an internal layered race-canvas banner. |
61
+ | Loading overlays | `templateUtils.js` | Component labels differ | Intentional accessibility labels per surface. |
62
+ | Unsupported-size overlays | `templateUtils.js` plus `layoutSupport.js` | Component labels and min-size thresholds differ | Intentional because each component has a different support envelope. |
63
+ | Sector bar markup | `templateUtils.js` | Wrapper and bar classes differ between sector telemetry, sector banner, and race-data telemetry detail | Shared helper owns selectors and initial fill/time markup; call sites pass only their intentional class differences. |
64
+ | Component styling scope | `PaddockSimulatorController.mountComponent()` and shell root class | All-in-one shell sets root class in template, composable controller sets it on host roots | Intentional because all-in-one owns the root element, while composable hosts provide roots. |
65
+ | DOM selector lookup | `domBindings.js` | `raceOverlayClearanceSupport.js` and `layoutSupport.js` perform local measurement queries | Intentional: measurement helpers need scoped live layout queries and should not enlarge app readout bindings. Timing tower state uses plural cached bindings because duplicate towers are package-owned runtime readouts. `resolveNodes()` centralizes plural collection plus singular fallback handling so renderers and resize observers do not duplicate that control flow. Overlay clearance caches static panel nodes at install time; only timing-row queries remain live because timing rows are renderer-owned dynamic markup. |
66
+ | Text and CSS custom-property writes | `domBindings.js` | Direct property assignment remains for non-custom style properties such as `style.color` | `setText()` / `setTextAll()` and `setStyleProperty()` skip unchanged writes in frequent render paths. Readout and banner renderers should use these helpers for package text and CSS custom properties instead of local write guards. |
67
+ | Car/driver overview field rendering | `domBindings.js` and `carOverviewRenderer.js` | Direct renderer fallback still supports narrow unit stubs without full DOM binding | Intentional compatibility fallback. Runtime mounts use cached field label/value bindings from `domBindings.js`, so changed overview renders do not rediscover static field children. |
68
+ | Control active state | `controlStateRenderer.js` | Event binding remains in `F1SimulatorApp` | Intentional separation: app owns events/state mutation; renderer owns button state. |
69
+ | Race-data lower-third and standalone panel | `bannerTemplates.js`, `domBindings.js`, and `raceDataBannerController.js` | Standalone adds one class and canvas embeds rely on placement | Intentional public placement difference with shared markup. Runtime state, open/dismiss controls, project mode, radio mode, and hide behavior fan out through grouped panel bindings so embedded and standalone panels do not drift. |
70
+ | Timing tower standalone vs embedded | `timingTowerTemplate.js`, `raceCanvasTemplate.js`, `timingTowerRenderer.js`, `raceStatusRenderer.js` | Embedded adds reveal button/id and narrow classes in race canvas | Intentional because disclosure state belongs to the parent race canvas, not the tower. Runtime rows, lap counters, race-control status, and gap labels are shared across standalone and embedded towers through plural DOM bindings and focused readout renderers. |
71
+ | Drawer and race-canvas responsive logic | `telemetryDrawerTemplate.js`, `raceCanvasTemplate.js`, `layoutSupport.js`, `raceOverlayClearanceSupport.js`, `layoutScheduler.js` | Drawer toolbar and race-canvas narrow reveal have distinct CSS classes | Intentional because they solve different container constraints while sharing component children. RAF scheduling/cancellation for layout measurement is shared so support checks and overlay clearance do not drift. |
72
+ | Local preview route setup | `local-preview/src/main.js` | Route-specific controllers, timers, readouts, and the components-page `COMPONENT_PIECE_MOUNTS` table remain local | Intentional host-owned demo behavior. Package component logic must not move into preview. The components-page table centralizes preview wiring of public mount helpers without changing package contracts. |
package/docs/concepts.md CHANGED
@@ -55,7 +55,7 @@ The composable controller has a setup phase:
55
55
 
56
56
  After startup, both controller styles expose runtime methods such as `restart()`, `selectDriver()`, `setSafetyCarDeployed()`, `getSnapshot()`, and `destroy()`.
57
57
 
58
- Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2)`. `0` means no pending pit request, `1` means take the next pit entry only if it is free, and `2` means keep the request active until the automatic pit-stop sequence completes.
58
+ Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2, targetCompound?)` after enabling `rules.modules.pitStops`. `0` clears a pending request, `1` keeps trying until a free-enough pit-entry window is available, and `2` commits to entering at the next pit-entry window even when opportunistic capacity or gap checks would block mode `1`. `targetCompound` must be one of `rules.modules.tireStrategy.compounds`; `tireStrategy` controls tire choices and stewarding, but does not enable pit stops by itself.
59
59
 
60
60
  ## Driver
61
61
 
@@ -81,13 +81,13 @@ Entry responsibilities:
81
81
  - Timing name.
82
82
  - Driver ratings.
83
83
  - Vehicle ratings.
84
- - Optional team object with `id`, `name`, `color`, and `icon`.
84
+ - Optional team object with `id`, `name`, `color`, `icon`, and `theme`.
85
85
  - Driver and vehicle rating components for the car/driver overview panel.
86
86
  - Optional driver and vehicle `customFields` for extra overview metadata.
87
87
 
88
88
  ## Team
89
89
 
90
- A team is entry-level metadata used for race identity and future pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted.
90
+ A team is entry-level metadata used for race identity, theme selection, and current pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted. `team.theme` can point to a named package theme for team-aware component selectors. When pit stops are enabled, team color marks the shared service area, queue point, and garage boxes, and optional `team.pitCrew` stats can affect service-time variability.
91
91
 
92
92
  ## Driver Ratings
93
93
 
@@ -127,7 +127,7 @@ The package supports:
127
127
 
128
128
  Browser mounts that omit `trackSeed` create a fresh procedural track for that mount. Passing `trackSeed` makes the generated circuit deterministic; repeated procedural seeds are cached within the page runtime. Generated circuits start from seeded connected coarse regions, trace the region boundary, then smooth and warp that boundary into the sampled centerline. This lets tracks move inward and outward through concave sections and chicane-like bends instead of degrading into circular or oval fallback layouts. Failed candidates retry with another deterministic region-derived shape.
129
129
 
130
- Every built track also exposes a deterministic `pitLane` near the start/finish straight. The pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, and track-limit stewarding. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
130
+ Tracks whose resolved generation options enable pit lanes expose a deterministic `pitLane` near the start/finish straight. The `race` profile includes pit-lane geometry; training profiles are pitless by default unless the host explicitly enables a pit lane. An enabled pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, track-limit stewarding, and stalled-DNF timing. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
131
131
 
132
132
  ## Progress
133
133
 
@@ -161,7 +161,9 @@ Per-car timing exposes both interval to the car ahead and direct same-lead-lap g
161
161
 
162
162
  A ruleset is a named preset for race-rule defaults. `paddock` is the package default, `grandPrix2025` / `fia2025` are 2024-2025-era grand-prix-style presets, and `custom` is for host-owned behavior.
163
163
 
164
- A rule module is an advanced subsystem under `rules.modules`, such as pit stops, tire strategy, tire degradation, stalled off-track DNF, penalties, weather, reliability, or fuel load. Presets set defaults, but explicit module config wins.
164
+ A rule module is an advanced subsystem under `rules.modules`. Active modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, and penalties. Weather, reliability, and fuel load are reserved placeholders and do not currently change grip, power, failure risk, mass, or pace. Presets set defaults, but explicit module config wins.
165
+
166
+ `pitStops` and `tireStrategy` are intentionally separate. `pitStops.enabled` owns automatic pit routing, pit-lane status, and successful pit intent requests. `tireStrategy.enabled` owns compound lists, tire-requirement stewarding, and pit target choices. A two-compound rule does not make cars pit unless `pitStops` is enabled separately or the selected ruleset preset enables both modules.
165
167
 
166
168
  Stalled off-track DNF is an opt-in retirement rule. When `rules.modules.stalledDnf.enabled` is `true`, it marks a car DNF when the car is off legal racing/pit surfaces and remains below the configured speed threshold for the configured time, while excluding pre-start, red flag, active pit handling, and already-finished cars.
167
169
 
@@ -137,7 +137,7 @@ const env = createPaddockEnvironment({
137
137
  drivers,
138
138
  entries,
139
139
  controlledDrivers: ['agent-1', 'agent-2'],
140
- physicsMode: 'simulator',
140
+ physicsMode: 'advanced',
141
141
  participantInteractions: { defaultProfile: 'batch-training' },
142
142
  observation: {
143
143
  profile: 'physical-driver',
@@ -177,7 +177,7 @@ import {
177
177
  const simulator = await mountF1Simulator(root, {
178
178
  drivers,
179
179
  entries,
180
- physicsMode: 'simulator',
180
+ physicsMode: 'advanced',
181
181
  expert: {
182
182
  enabled: true,
183
183
  controlledDrivers: ['agent-1'],
@@ -14,7 +14,7 @@ mountF1Simulator(root, {
14
14
  onDriverOpen,
15
15
  seed,
16
16
  trackSeed,
17
- trackQueryIndex,
17
+ trackGeneration,
18
18
  warmup,
19
19
  totalLaps,
20
20
  physicsMode,
@@ -29,6 +29,7 @@ mountF1Simulator(root, {
29
29
  backLinkLabel,
30
30
  showBackLink,
31
31
  ui,
32
+ debug,
32
33
  assets,
33
34
  expert,
34
35
  onLoadingChange,
@@ -50,7 +51,7 @@ const simulator = createPaddockSimulator({
50
51
  onDriverOpen,
51
52
  seed,
52
53
  trackSeed,
53
- trackQueryIndex,
54
+ trackGeneration,
54
55
  warmup,
55
56
  totalLaps,
56
57
  physicsMode,
@@ -61,6 +62,7 @@ const simulator = createPaddockSimulator({
61
62
  preset,
62
63
  theme,
63
64
  ui,
65
+ debug,
64
66
  assets,
65
67
  expert,
66
68
  });
@@ -114,6 +116,7 @@ const env = createPaddockEnvironment({
114
116
  standingStart: false,
115
117
  modules: {
116
118
  penalties: {
119
+ enabled: true,
117
120
  trackLimits: { strictness: 0.25 },
118
121
  },
119
122
  },
@@ -133,7 +136,7 @@ result = env.step({
133
136
  `controlledDrivers` is required. It supports one or many externally controlled cars. Non-controlled participants use the built-in driver AI in the stable 1.0 environment API.
134
137
  `externalRenderer` is optional and observer-only. It can be a function or `{ onFrame(frame) }`, and receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`. The hook does not mutate simulation state and hook failures are isolated so stepping continues.
135
138
  `warmup` is optional and enabled by default. It primes a disposable runtime during load/reset creation and caches by configuration fingerprint so repeated identical resets skip warmup. Use `warmup: { enabled, policy: 'config-change' | 'always' | 'never', steps }` or `warmup: false` to disable.
136
- `physicsMode` is optional and accepts `'arcade'` or `'simulator'`. Invalid values fall back to `'arcade'`, which is the default compatibility mode. `'simulator'` keeps the same public action contract but uses stricter 2D velocity/yaw dynamics and exposes additional telemetry fields on snapshots and observations: `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. In simulator mode, `slipAngleRadians` is derived from the car heading versus actual velocity direction, and mixed wheel surfaces are averaged for physics while wheel snapshots still report each contact patch.
139
+ `physicsMode` is optional and accepts `'arcade'` or `'advanced'`. Invalid values, including the removed old `'simulator'` name, fall back to `'arcade'`, which is the default compatibility mode. `'advanced'` keeps the same public action contract but uses stricter 2D velocity/yaw dynamics and exposes additional telemetry fields on snapshots and observations: `velocityX`, `velocityY`, `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. In advanced mode, `slipAngleRadians` is derived from the car heading versus actual velocity direction, and mixed wheel surfaces are averaged for physics while wheel snapshots still report each contact patch. Terminal and race-control-held advanced-mode cars publish `velocityX: 0` and `velocityY: 0` whenever scalar `speed` is zero, so training loops do not see stale pre-DNF, pre-grid, pre-red-flag, or pre-queue motion. Arcade snapshots publish `velocityX` and `velocityY` as `null`; consumers that need a heading-aligned arcade velocity should derive it from `heading` and `speed`.
137
140
  `rules` is an optional override object for the race rules documented in [rules.md](rules.md). Flat keys such as `standingStart: false` still work for existing behavior. Advanced systems live under `rules.modules` so hosts can choose a preset and then override individual modules:
138
141
 
139
142
  ```js
@@ -148,6 +151,10 @@ rules: {
148
151
  tirePitRequestThresholdPercent: 50,
149
152
  tirePitCommitThresholdPercent: 30,
150
153
  },
154
+ tireStrategy: {
155
+ enabled: true,
156
+ mandatoryDistinctDryCompounds: 2,
157
+ },
151
158
  tireDegradation: {
152
159
  enabled: false,
153
160
  },
@@ -166,7 +173,7 @@ rules: {
166
173
  }
167
174
  ```
168
175
 
169
- Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. The `fia2025` name is a 2024-2025-era grand-prix-style package preset; explicit module config always wins over preset defaults. `rules.modules.tireDegradation.enabled: false` freezes tyre energy for deterministic training or visual comparison without changing tire compounds or pit rules. `rules.modules.stalledDnf` defaults to `{ enabled: false, maxStoppedSeconds: 12, speedThresholdKph: 5 }`; set `enabled: true` to retire stuck off-track cars with `dnfReason: 'stalled-off-track'`. Pre-start, red flag, legal pit surfaces, active pit handling, and already-finished cars are excluded. Penalty strictness is clamped from `0` to `1`, where `0` disables enforcement for that subsection and `1` uses the configured rule margin. `rules` is not a direct state-mutation API.
176
+ Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. The `fia2025` name is a 2024-2025-era grand-prix-style package preset; explicit module config always wins over preset defaults. `rules.modules.pitStops.enabled` is the switch for automatic pit routing, pit-lane status, and successful `setPitIntent()` calls. `rules.modules.tireStrategy.enabled` is separate: it controls available compounds, tire-requirement stewarding, and pit target choices, but it does not enable pit stops. Use `ruleset: 'fia2025'` / `ruleset: 'grandPrix2025'` or explicitly enable both modules when a host wants a two-compound race with automatic stops. `rules.modules.tireDegradation.enabled: false` freezes tyre energy for deterministic training or visual comparison without changing tire compounds or pit rules. `rules.modules.stalledDnf` defaults to `{ enabled: false, maxStoppedSeconds: 12, speedThresholdKph: 5 }`; set `enabled: true` to retire stuck off-track cars with `dnfReason: 'stalled-off-track'` only when they are off legal racing or pit surfaces. Pre-start, red flag, legal pit surfaces, active pit handling, and already-finished cars are excluded. Penalty strictness is clamped from `0` to `1`, where `0` disables enforcement for that subsection and `1` uses the configured rule margin. `rules` is not a direct state-mutation API.
170
177
 
171
178
  Scenario support:
172
179
 
@@ -198,7 +205,7 @@ scenario: {
198
205
 
199
206
  Scenario placement is applied only during environment creation/reset through the simulator state API. It does not give policies a state-mutation path during `step(actions)`. Explicit `placements` override preset placement for the same driver, and `traffic` places another participant relative to a placed or existing car. `env.reset(partialOptions)` preserves omitted nested option groups, but an explicitly supplied `scenario.placements` object replaces the old placement map so callers can clear stale reset placements with `{ scenario: { placements: {} } }`. Static obstacles, debug mutation, assisted controls, and Python Gymnasium wrappers are intentionally deferred. Replay ghosts and participant interaction profiles are supported separately below. The supported package boundary today is JavaScript Gym-style control plus a JSON worker protocol, not a Python Gym package and not a scenario editor.
200
207
 
201
- `env.resetDrivers(placements)` applies the same placement shape to selected controlled drivers without recreating the whole environment. It is an episode-boundary API for batched training: selected drivers get a new `episodeId`, `episodeStep: 0`, cleared manual controls, and cleared pit intent. Non-selected controlled drivers keep their current car state and episode counters. Max-step truncation is evaluated from each reported driver's own `episodeStep`, so resetting one truncated driver clears that driver's done state without rewinding the environment `info.step` or changing other drivers' episode state. Reset placement is still validated against the normal track/runoff/barrier model before observations are returned. If a reset placement puts a car into the barrier destruction boundary, the returned result reports that driver as `destroyed` with `endReason: 'destroyed'` and stable miss-valued rays instead of doing an expensive far-out sensor scan. This API is not accepted inside `step(actions)`.
208
+ `env.resetDrivers(placements)` applies the same placement shape to selected controlled drivers without recreating the whole environment. It is an episode-boundary API for batched training: selected drivers get a new `episodeId`, `episodeStep: 0`, cleared manual controls, and cleared pit intent. Non-selected controlled drivers keep their current car state and episode counters. Max-step truncation is evaluated from each reported driver's own `episodeStep`, so resetting one truncated driver clears that driver's done state without rewinding the environment `info.step` or changing other drivers' episode state. If all reported controlled drivers are ended, top-level `done` is true even when their terminal states are mixed, such as one destroyed driver and one max-step-truncated driver. Reset placement is still validated against the normal track/runoff/barrier model before observations are returned. If a reset placement puts a car into the barrier destruction boundary, the returned result reports that driver as `destroyed` with `endReason: 'destroyed'` and stable miss-valued rays instead of doing an expensive far-out sensor scan. With reset-only observation scope, top-level terminal state is evaluated over the reported reset drivers but `info.drivers` still reflects each controlled driver's own state, so a destroyed reset driver does not falsely terminate healthy non-reset drivers. This API is not accepted inside `step(actions)`.
202
209
 
203
210
  Participant interaction profiles are simulator-owned environment setup, not RL logic. They let hosts run multiple real cars in one environment without forcing every car to collide with or appear in every other car's sensors:
204
211
 
@@ -254,7 +261,7 @@ replayGhosts: [
254
261
  ]
255
262
  ```
256
263
 
257
- Replay ghosts appear in `snapshot.replayGhosts`, interpolate by `timeSeconds`, render as translucent browser overlays, and never enter `snapshot.cars`, timing rows, race order, pit logic, collision resolution, or steward penalties. They are hidden from rays and nearby-car observations by default. If `sensors.detectableByRays` or `sensors.detectableAsNearby` is explicitly enabled, observations may report that replay ghost as a sensor target with `targetType` / `entityType: 'replayGhost'`; it still remains a non-physics reference entity.
264
+ Replay ghosts appear in `snapshot.replayGhosts`, interpolate by `timeSeconds`, render as translucent browser overlays, and never enter `snapshot.cars`, timing rows, race order, pit logic, collision resolution, or steward penalties. They are hidden from rays and nearby-car observations by default. If `sensors.detectableByRays` or `sensors.detectableAsNearby` is explicitly enabled, observations may report that replay ghost as a sensor target with `targetType` / `entityType: 'replayGhost'`; it still remains a non-physics reference entity. Sensor target filtering and enrichment treat `{ entityType, id }` as the target identity, so a replay ghost id collision cannot replace a real car's nearby-radar velocity facts and an opt-in same-id reference ghost remains visible to that car's sensors.
258
265
 
259
266
  Actions use normalized low-level controls:
260
267
 
@@ -339,7 +346,7 @@ const actionSpec = env.getActionSpec();
339
346
  const observationSpec = env.getObservationSpec();
340
347
  ```
341
348
 
342
- `actionSpec` describes controlled drivers, normalized action ranges, and the optional pit intent values. `observationSpec` describes object observation fields, ray layout, nearby-car limits, track lookahead fields, and the versioned vector schema. `observation.lookaheadMeters` is sanitized to a finite numeric array; invalid or empty values fall back to the default `[20, 50, 100, 150]`. The opt-in `observation.profile: 'physical-driver'` profile defaults lookahead to `[]` so policies can use local driver-like senses without receiving privileged future track curvature. Realistic training runs should pair this profile with `physicsMode: 'simulator'`; that keeps the model's observed yaw, grip, contact-patch, kerb, and runoff behavior aligned with the physics it is learning to control.
349
+ `actionSpec` describes controlled drivers, normalized action ranges, and the optional pit intent values. `observationSpec` describes object observation fields, ray layout, nearby-car limits, track lookahead fields, and the versioned vector schema. `observation.lookaheadMeters` is sanitized to a finite numeric array; invalid or empty values fall back to the default `[20, 50, 100, 150]`. The opt-in `observation.profile: 'physical-driver'` profile defaults lookahead to `[]` so policies can use local driver-like senses without receiving privileged future track curvature. Realistic training runs should pair this profile with `physicsMode: 'advanced'`; that keeps the model's observed yaw, grip, contact-patch, kerb, and runoff behavior aligned with the physics it is learning to control.
343
350
 
344
351
  Observation output can be compacted for training throughput:
345
352
 
@@ -352,7 +359,20 @@ observation: {
352
359
  }
353
360
  ```
354
361
 
355
- The default remains `output: 'full'`, `includeSchema: true`, and `vectorType: 'array'`, which returns `{ object, vector, schema, events }` for backward compatibility. `output: 'vector'` returns `{ vector, events }` unless schema inclusion is requested. `output: 'object'` returns `{ object, events }` unless schema inclusion is requested. `getObservationSpec()` remains the canonical schema source for compact loops. `vectorType: 'float32'` returns a `Float32Array` for JavaScript consumers that want typed numeric buffers; JSON worker users should keep the default array output unless their bridge explicitly handles typed arrays. Destroyed or out-of-race cars keep the same ray/vector schema and return miss-valued ray channels so model code does not need a separate terminal tensor shape.
362
+ The default remains `output: 'full'`, `includeSchema: true`, and `vectorType: 'array'`, which returns `{ object, vector, schema, events }` for backward compatibility. `output: 'vector'` returns `{ vector, events }` unless schema inclusion is requested. `output: 'object'` returns `{ object, events }` unless schema inclusion is requested. `getObservationSpec()` remains the canonical schema source for compact loops. When `sensorsByDriver` changes ray or nearby-car shape for individual controlled drivers, `observationSpec.perDriver[driverId]` exposes that driver's exact ray, nearby-car, and vector schema shape. Full observations include the actual object ray geometry and Policy Runner object-backed encoding uses that geometry instead of reconstructing angles or lengths from a default layout. `vectorType: 'float32'` returns a `Float32Array` for JavaScript consumers that want typed numeric buffers; JSON worker users should keep the default array output unless their bridge explicitly handles typed arrays. Destroyed or out-of-race cars keep the same ray/vector schema and return miss-valued ray channels so model code does not need a separate terminal tensor shape.
363
+
364
+ The executable model-sense contract lives in `src/__tests__/environmentSenseContract.test.js` and is summarized in [Model Sense Contract](sense_contract.md). It treats the object observation as a policy-facing contract, checks it against simulator snapshot facts through independent unit and geometry formulas, then decodes each vector schema entry back to the object contract. A new vector field is not considered contract-covered until it has an oracle there.
365
+
366
+ Episode limits are configured separately from race length:
367
+
368
+ ```js
369
+ episode: {
370
+ maxSteps: 2400,
371
+ endOnRaceFinish: true,
372
+ }
373
+ ```
374
+
375
+ `episode.maxSteps` is a safety horizon for controlled-driver episodes, not the number of race laps. If all reported controlled drivers reach it, the result is `truncated: true`, `done: true`, and `endReason: 'max-steps'`. The default is intentionally high (`1_000_000` environment/expert steps) so normal browser expert sessions are unlikely to hit it; bounded training, evaluation, and smoke loops should pass a smaller explicit value. `episode.endOnRaceFinish` defaults to `true`, so race finish remains terminal unless a host explicitly disables that behavior.
356
376
 
357
377
  Result state output can also be compacted:
358
378
 
@@ -363,7 +383,7 @@ result: {
363
383
  }
364
384
  ```
365
385
 
366
- The default `stateOutput: 'full'` preserves the public `{ state: { snapshot } }` payload. `minimal` returns the lean observation snapshot used by the environment. `none` returns `state: null`, so external loops should rely on `observation`, `metrics`, and `info` instead. `resetDriversObservationScope: 'reset'` makes `resetDrivers()` return observations and metrics only for reset drivers by default; callers can override per call with `env.resetDrivers(placements, { observationScope: 'all' | 'reset', stateOutput })`. Partial `env.reset(options)` calls merge plain nested option groups such as `rules.modules`, `trackGeneration`, `sensors`, `observation`, `result`, and `episode`; arrays and explicit placement maps replace the old value.
386
+ The default `stateOutput: 'full'` preserves the public `{ state: { snapshot } }` payload. `minimal` returns the lean observation snapshot used by the environment. `none` returns `state: null`, so external loops should rely on `observation`, `metrics`, and `info` instead. `resetDriversObservationScope: 'reset'` makes `resetDrivers()` return observations, metrics, and `info.controlledDrivers` only for reset drivers by default; callers can override per call with `env.resetDrivers(placements, { observationScope: 'all' | 'reset', stateOutput })`. `info.drivers` still carries episode state for the configured controlled drivers so active, non-reset drivers keep their episode ids and terminal flags. `createPaddockDriverControllerLoop()` maintains an observation cache across partial resets and merges reset-driver observations into it before the next controller decision. Partial `env.reset(options)` calls merge plain nested option groups such as `rules.modules`, `trackGeneration`, `sensors`, `observation`, `result`, and `episode`; arrays and explicit placement maps replace the old value.
367
387
 
368
388
  The JSON worker protocol exposes the same reset-result controls through `resultOptions`:
369
389
 
@@ -419,7 +439,7 @@ reward({ driverId, previous, current, action, events, state, metrics, episode })
419
439
  }
420
440
  ```
421
441
 
422
- `metrics` is the same neutral per-driver metrics object returned in `result.metrics[driverId]`, and `episode` is the same per-driver runtime state returned in `result.info.drivers[driverId]`. These are package-owned facts, not reward policy. If no reward callback is provided, `result.reward` is `null`. If a callback returns `undefined`, `null`, `NaN`, or an infinite value, that driver's reward is normalized to `0`. PaddockJS does not infer, select, or tune rewards.
442
+ `metrics` is the same neutral per-driver metrics object returned in `result.metrics[driverId]`, and `episode` is the same per-driver runtime state returned in `result.info.drivers[driverId]`. These are package-owned facts, not reward policy. Reward callbacks run only for `step(actions)` transition results; reset/read paths such as `reset()`, `resetDrivers()`, and cold `getObservation()` are reward-neutral and return no transition reward. If no reward callback is provided, `result.reward` is `null`. If a callback returns `undefined`, `null`, `NaN`, or an infinite value, that driver's reward is normalized to `0`. The starter `createProgressReward()` reads speed/legal-surface facts from the reward snapshot and metrics when object observations are omitted, so the same transition keeps the same demo reward in compact vector-only mode. PaddockJS does not infer, select, or tune rewards.
423
443
 
424
444
  Neutral rollout recording is available for external training loops:
425
445
 
@@ -433,7 +453,7 @@ const next = env.step(action);
433
453
  recorder.recordStep(previous, action, next);
434
454
  ```
435
455
 
436
- Each recorded transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. This is data export only; it does not train or update a model.
456
+ Each recorded transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. Recorded transitions are cloned at record time, including numeric arrays and typed vectors, so later mutation of a policy action object or observation buffer cannot change already-recorded samples. This is data export only; it does not train or update a model.
437
457
 
438
458
  Deterministic evaluation helpers run fixed seeds/scenarios and report environment quality metrics:
439
459
 
@@ -560,9 +580,9 @@ snapshot.cars = physics-driven race participants
560
580
  snapshot.replayGhosts = trajectory-driven replay/reference entities
561
581
  ```
562
582
 
563
- Observation objects use physical units such as kph, meters/second, meters, and radians. Optional `vector` values use fixed documented scaling from `schema`; they do not use hidden per-car normalization. Full simulator truth remains available under `state.snapshot`. Internally, the environment avoids rebuilding full snapshots during each `frameSkip` substep and defaults to the non-public track query index unless `trackQueryIndex: false` is set, but the returned `state.snapshot`, reward callback `previous` snapshot, and reward callback `state.snapshot` keep the same public shape.
583
+ Observation objects use physical units such as kph, meters/second, meters, and radians. Optional `vector` values use fixed documented scaling from `schema`; they do not use hidden per-car normalization. Full simulator truth remains available under `state.snapshot`. Internally, the environment avoids rebuilding full snapshots during each `frameSkip` substep and always uses the non-public track query index for track, surface, pit-lane, and ray queries, but the returned `state.snapshot`, reward callback `previous` snapshot, and reward callback `state.snapshot` keep the same public shape.
564
584
 
565
- The environment observation now exposes local physical driver senses separately from full snapshot truth. `self.yawRateRadiansPerSecond` is the car's current yaw rate. `self.appliedControls` reports the normalized controls that actually drove the latest physics step: `steering` in `-1..1`, `steeringRadians` in simulator radians, `throttle` in `0..1`, and `brake` in `0..1`. For controlled cars this mirrors the accepted environment action; in `actionPolicy: 'report'` mode, a missing or invalid vehicle action releases stale manual controls, lets the built-in AI drive that step, and exposes its exact controls for auditing or clean local imitation datasets. `trackRelation` gives immediate local road relationship: lateral offset, heading error, legal width, left/right boundary distance, legal-surface state, and current surface. `contactPatches` exposes the four wheel/contact-patch surface readings as stable public observation data, with `surfaceCode` intended only as a compact vector encoding.
585
+ The environment observation now exposes local physical driver senses separately from full snapshot truth. `self.yawRateRadiansPerSecond` is the car's current yaw rate. `self.appliedControls` reports the normalized controls that actually drove the latest physics step: `steering` in `-1..1`, `steeringRadians` in simulator radians, `throttle` in `0..1`, and `brake` in `0..1`. For controlled cars this mirrors the accepted environment action; in `actionPolicy: 'report'` mode, a missing or invalid vehicle action releases stale manual controls, lets the built-in AI drive that step, and exposes its exact controls for auditing or clean local imitation datasets. `trackRelation` gives immediate local road relationship: lateral offset, heading error, legal width, left/right boundary distance, legal-surface state, and current surface. `contactPatches` exposes the four wheel/contact-patch surface readings as stable public observation data. Compact physical-driver vectors include each patch's `surfaceCode`, `signedOffsetMeters`, and `crossTrackErrorMeters` so vector-only training loops do not lose a contact-patch sense that exists in object observations.
566
586
 
567
587
  Default rays use a compact center-origin set with forward, side, and rear awareness:
568
588
 
@@ -616,9 +636,9 @@ sensors: {
616
636
 
617
637
  Ray precision defaults to `driver`. Driver precision is the active model-facing sensor contract and uses the normal sampled ray step without extra refinement. `precision: 'debug'` is available for clearly labeled diagnostics with additional edge refinement, but debug precision must not be displayed as model senses unless the policy is also running with that exact sensor config.
618
638
 
619
- Surface channels are computed only when requested. `kerb` is legal racing surface. `illegalSurface` reports the first grass or gravel surface crossed by the ray. The same model-facing ray contract is accelerated for both normal on-track driving and nearby off-track recovery starts using indexed ray-boundary intersections. Ambiguous pit-connector and unusual geometry cases may use indexed sampled fallback internally, but the returned ray object shape is unchanged. Barrier walls are not exposed as a model-facing ray target; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Barrier contact is enforced as a shared destruction boundary in both physics modes at the rendered wall's inner face and reported through events, snapshots, metrics, and per-driver episode state.
639
+ Surface channels are computed only when requested. `kerb` is legal racing surface. `illegalSurface` reports the first grass or gravel surface crossed by the ray. Requested surface-ray channels are part of the active object observation and compact vector schema for both default and physical-driver profiles. `precision: 'driver'` road-edge, kerb, and illegal-surface channels use the canonical indexed track projection path with driver-precision validation, preserving zero-distance origin surface hits and avoiding debug-only refinement in policy inputs. Requested surface channels preserve an origin hit at `distanceMeters: 0` even if another requested channel has no visible boundary. Ambiguous pit-connector and unusual geometry cases may use sampled validation internally, but the returned ray object shape is unchanged. Barrier walls are not exposed as a model-facing ray target; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Barrier contact is enforced as a shared destruction boundary in both physics modes at the rendered wall's inner face and reported through events, snapshots, metrics, and per-driver episode state.
620
640
 
621
- Nearby-car observations are car-relative:
641
+ Nearby-car observations are car-relative. When advanced-mode velocity fields are present, `closingRateMetersPerSecond` and `timeToContactSeconds` use the actual 2D velocity vector instead of assuming the car is moving in its heading direction, so slip and recovery states remain physically accurate:
622
642
 
623
643
  ```js
624
644
  {
@@ -639,6 +659,7 @@ Nearby-car observations are car-relative:
639
659
  ```
640
660
 
641
661
  If no `reward` callback is provided, `result.reward` is `null`. If provided, rewards are returned by controlled driver ID. The callback receives `driverId`, `previous`, `current`, `action`, `events`, `state.snapshot`, `metrics`, and `episode`.
662
+ Reward callbacks run only for `step(actions)` transition results. Reset and read paths return `reward: null`, so episode setup, `resetDrivers()`, and `getObservation()` cannot create fake replay rewards or trigger reward side effects.
642
663
 
643
664
  Browser expert mode is opt-in through the browser mount API:
644
665
 
@@ -719,9 +740,11 @@ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timin
719
740
 
720
741
  Profiles are presets, not separate generators. `race` preserves the default full-length circuit with pit lane; `training-short`, `training-medium`, and `training-technical` generate smaller pitless circuits for training or demos. Resolution is `race` defaults, selected profile defaults, then explicit overrides. Procedural tracks are generated by tracing, smoothing, and warping package-owned seeded region masks, then validating the resulting centerline instead of falling back to pure ovals. Invalid candidates are rejected for length, world bounds, non-adjacent clearance, turn sharpness, self-intersections, and weak shape variation. The start/finish area is normalized into an explicit straight window so the starting grid, pit-entry approach, and immediate pit-exit merge area are straight even when the rest of the circuit is generated from curved controls.
721
742
 
722
- `trackQueryIndex` controls indexed track/surface query acceleration. Browser/expert simulator mounts, headless environments, and direct race-simulation construction default this option to `true`, so visual race simulation, expert controls, Policy Runner playback, headless training/evaluation runs, and model-sense visualization use the indexed path by default. Set `trackQueryIndex: false` to force legacy non-indexed queries for comparison or debugging. It does not change public snapshot or observation shapes.
743
+ Indexed track/surface queries are the canonical runtime path for browser/expert simulator mounts, headless environments, direct race-simulation construction, Policy Runner playback, training/evaluation runs, and model-sense visualization. The index is internal infrastructure and does not change public snapshot or observation shapes.
723
744
 
724
- `initialCameraMode` is optional and accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`. Invalid values fall back to `'leader'`. The `overview` camera frames the generated track bounds with package-owned padding and pit-lane extent. The `pit` camera frames the active track's `pitLane` geometry, zooms out when needed to keep the full pit lane inside the active race-view safe area, and falls back to `leader` when no pit lane is available. Camera zoom controls and wheel zoom apply to every mode, with zoom-out bounded by the active track frame.
745
+ `backLinkHref` is optional and controls the package-owned top-bar back link in the all-in-one shell and race-controls component. It accepts relative URLs, hash URLs, and absolute `http:` / `https:` URLs. Unsafe schemes such as `javascript:` and malformed URL values are replaced with the package default. Driver, team, snapshot, and radio colors are validated before they are written as runtime CSS values; unsafe color strings fall back to package defaults.
746
+
747
+ `initialCameraMode` is optional and accepts `'overview'`, `'leader'`, `'selected'`, `'driver'`, `'show-all'`, or `'pit'`. Invalid values fall back to `'leader'`. The `overview` camera frames the generated track bounds with package-owned padding and pit-lane extent. The `driver` camera follows the selected car from a lower screen anchor and rotates the world so the selected car points upward; its control is opt-in through `ui.driverCamera: true`, and setting `initialCameraMode: 'driver'` enables that control automatically. The `pit` camera frames the active track's `pitLane` geometry, zooms out when needed to keep the full pit lane inside the active race-view safe area, and falls back to `leader` when no pit lane is available. Camera zoom controls and wheel zoom apply to every mode, with zoom-out bounded by the active track frame.
725
748
 
726
749
  Each driver must have:
727
750
 
@@ -803,6 +826,7 @@ Entries are optional. If omitted, defaults are used.
803
826
  name: 'Ledger Racing',
804
827
  color: '#00ff84',
805
828
  icon: 'LR',
829
+ theme: 'trackside',
806
830
  pitCrew: {
807
831
  speed: 0.72,
808
832
  consistency: 0.81,
@@ -838,7 +862,7 @@ onDriverOpen(driver) {
838
862
  }
839
863
  ```
840
864
 
841
- The callback receives the normalized driver object. If the host wants modals, routing, analytics, or external tabs, it should implement that inside this callback.
865
+ The callback receives the normalized driver object. If the host wants modals, routing, analytics, or external tabs, it should implement that inside this callback. `onDriverOpen` is host-owned navigation code and should handle its own failures; unlike lifecycle callbacks, it is not routed through `onError`.
842
866
 
843
867
  Optional lifecycle callbacks:
844
868
 
@@ -854,10 +878,12 @@ Optional lifecycle callbacks:
854
878
  }
855
879
  ```
856
880
 
857
- `onRaceEvent` receives simulation events such as `contact`, `penalty`, `track-limits`, `pit-lane-speeding`, `car-dnf`, `safety-car`, `green-flag`, `start-lights-out`, and `race-finish`. `car-dnf` currently reports stalled off-track retirements with `reason: 'stalled-off-track'`. `contact` events include metadata from the production body collision solver: `firstShapeId`, `secondShapeId`, `contactType`, `depth`, and `timeOfImpact`. Host callback errors are caught; if `onError` exists, it receives `{ callback: name }` context for callback failures.
881
+ `onRaceEvent` receives simulation events such as `contact`, `penalty`, `track-limits`, `pit-lane-speeding`, `car-dnf`, `safety-car`, `green-flag`, `start-lights-out`, and `race-finish`. `car-dnf` currently reports stalled off-track retirements with `reason: 'stalled-off-track'`. `contact` events include metadata from the production body collision solver: `firstShapeId`, `secondShapeId`, `contactType`, `depth`, and `timeOfImpact`. Lifecycle callback errors are caught; if `onError` exists, it receives `{ callback: name }` context for callback failures.
858
882
 
859
883
  Race snapshots include a top-level `penalties` array. Each penalty entry includes `id`, `type`, `driverId`, `strictness`, `status`, `penaltySeconds`, `pendingPenaltySeconds`, `serviceType`, `serviceRequired`, `serviceServedAt`, `appliedAt`, `cancelledAt`, `unserved`, `positionDrop`, `gridDrop`, `disqualified`, `consequences`, `lap`, `at`, and rule-specific context such as `otherCarId`, `aheadDriverId`, `atFaultDriverId`, `sharedFault`, and `impactSpeedKph` for collision penalties or `speedKph`, `speedLimitKph`, `excessKph`, and `pitLanePart` for pit-lane speeding penalties. Clear rear contact has one at-fault driver; unclear meaningful contact records one shared-fault penalty per involved driver. Multiple time penalties for the same driver are summed into the car snapshot's `penaltySeconds` and adjusted finish/classification time.
860
884
 
885
+ Each full public car snapshot includes `trackState` for the car-center track classification used by runtime readouts and environment surfaces. Its stable public fields are `distance`, `signedOffset`, `crossTrackError`, `surface`, `inPitLane`, `pitLanePart`, `pitBoxId`, `curvature`, and `heading`. `pitLanePart` and `pitBoxId` are `null` when the car is not classified on a pit-lane part or pit box. The existing flat `signedOffset`, `crossTrackError`, `surface`, `inPitLane`, `pitLanePart`, and `pitBoxId` fields remain as convenience mirrors for common UI and host checks.
886
+
861
887
  Penalty status values are `issued`, `served`, `applied`, and `cancelled`. Time, position-drop, grid-drop, and disqualification consequences are immediate `applied` penalties. Drive-through and stop-go consequences are service penalties: they start as `issued`, can be completed with `servePenalty(penaltyId)`, and convert to applied time if unserved when final classification is calculated. Pit stops also serve eligible penalties before tire work starts: applied time penalties add their seconds as a hold, stop-go penalties add their configured service seconds, and drive-through penalties are marked served by the pit-lane traversal without extra stationary hold time.
862
888
 
863
889
  ## Asset Overrides
@@ -923,6 +949,9 @@ ui: {
923
949
  raceDataBannerSize: 'custom',
924
950
  raceDataTelemetryDetail: false,
925
951
  timingTowerVerticalFit: 'expand-race-view',
952
+ timingGapMode: 'interval',
953
+ timingGapModeToggle: true,
954
+ timingEntryVerticalPadding: 5,
926
955
  },
927
956
  debug: {
928
957
  physicsModeIndicator: false,
@@ -930,11 +959,11 @@ debug: {
930
959
  ```
931
960
 
932
961
  - `layoutPreset`: `'standard'` or `'left-tower-overlay'`. The overlay preset creates a left broadcast gutter inside the race canvas, places the timing tower in that gutter at the same width as the default timing-board column, and frames the camera around the remaining race-view area. Camera controls are external by default. In the combined shell, the project/radio lower-third stays inside the race window and can render over the timing sidebar.
933
- - `cameraControls`: `'embedded'`, `'external'`, or `false`. The default is external so camera controls do not cover the race view. Embedded controls render inside the race canvas only when explicitly requested. External controls are mounted with `mountCameraControls(root)` or included in package-owned workbench templates. The generated controls include mode buttons, zoom buttons, and a `Mute banners` toggle that temporarily disables project/radio lower-thirds while active. A browser-playback speed button is optional for normal camera controls through `ui.simulationSpeedControl: true`; the complete race workbench enables it by default and cycles `1x`, `2x`, `3x`, `4x`, `5x`, `10x`, then back to `1x`. `false` leaves camera controls unrendered, though callers can still drive selection through controller methods.
962
+ - `cameraControls`: `'embedded'`, `'external'`, or `false`. The default is external so camera controls do not cover the race view. Embedded controls render inside the race canvas only when explicitly requested. External controls are mounted with `mountCameraControls(root)` or included in package-owned workbench templates. The generated controls include mode buttons, zoom buttons, and a `Mute banners` toggle that temporarily disables project/radio lower-thirds while active. `ui.driverCamera: true` adds the opt-in `driver` camera button; it is omitted from generated controls by default. A browser-playback speed button is optional for normal camera controls through `ui.simulationSpeedControl: true`; the complete race workbench enables it by default and cycles `1x`, `2x`, `3x`, `4x`, `5x`, `10x`, then back to `1x`. `false` leaves camera controls unrendered, though callers can still drive selection through controller methods.
934
963
  - `showFps`: controls whether the race canvas renders the FPS readout.
935
964
  - `showRaceDataPanel`: controls whether the precombined shell includes the project/radio lower-third inside the race window.
936
965
  - `showTimingTower`, `showTelemetry`: reserved component visibility flags for host layout decisions.
937
- - `debug.physicsModeIndicator`: when `true`, renders a small top-left square in the race canvas. Blue means `physicsMode: 'arcade'`; red means `physicsMode: 'simulator'`. It defaults to `false` and is intended only for debug/development use.
966
+ - `debug.physicsModeIndicator`: when `true`, renders a small top-left square in the race canvas. Blue means `physicsMode: 'arcade'`; red means `physicsMode: 'advanced'`. It defaults to `false` and is intended only for debug/development use.
938
967
  - `telemetryIncludesOverview`: controls whether the telemetry stack template embeds the car/driver overview. Composable hosts can also pass `mountTelemetryPanel(root, { includeOverview: false })`.
939
968
  - `telemetryModules`: controls optional telemetry surfaces inside stack/drawer templates. The default object enables `core` scalar readouts, `sectors` progress bars, `lapTimes`, and `sectorTimes`. It can also be `false` to disable all telemetry modules, or an array such as `['sectors', 'lapTimes']` to render only named modules. These modules are also individually mountable with `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetryLapTimes`, and `mountTelemetrySectorTimes`.
940
969
  - `raceDataBanners.initial`: `'project'`, `'radio'`, or `'hidden'`. This controls which lower-third appears first in the precombined shell.
@@ -945,29 +974,84 @@ debug: {
945
974
  - `penaltyBanners`: when `true`, the race view shows a top steward message for track-limit warnings and new steward penalty decisions. Time penalties show a large `+10s` style chip in the left block, with the affected car and rule/reason beside it. Warning-only messages use warning colors and remain separate from penalty decisions. It does not replace the project/radio lower-third.
946
975
  - `timingPenaltyBadges`: when `true`, timing rows for penalized drivers show a red `!` badge with an accessible penalty label. Warning-only events do not count as penalties and do not show the badge.
947
976
  - `timingTowerVerticalFit`: `'expand-race-view'` lets the combined race window grow to contain the timing tower. `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same values can be passed to `mountRaceCanvas(root, { includeTimingTower: true, timingTowerVerticalFit })` for an embedded composable timing tower.
977
+ - `timingGapMode`: `'interval'` or `'leader'`. The default `'interval'` starts the tower in `Int` mode and shows the interval to the car ahead; `'leader'` starts the tower in `Gap` mode and shows the total gap to P1. Invalid values fall back to `'interval'`.
978
+ - `timingGapModeToggle`: defaults to `true`. When `true`, the timing tower renders one compact `Int`/`Gap` header toggle in the timing header's gap column. The toggle is package-owned UI: it shares the header alignment grid with `POS`, `TEAM`, `PROJECT`, and `TYRE`, preserves the timing tower's narrow broadcast proportions, keeps a practical `44px` target, and draws selected/focus styling inside the header instead of expanding the column. Set it to `false` when the host should control the mode only through `setTimingGapMode()`, `getTimingGapMode()`, or `toggleTimingGapMode()`.
979
+ - `timingEntryVerticalPadding`: defaults to `5`. This single non-negative number is applied in pixels above and below every timing row entry, letting hosts make the timing tower denser or looser without per-entry styling.
980
+ - `responsiveNarrowLayout`: defaults to `true`. Package templates use it to enable the narrow timing reveal and narrow drawer behavior when their container is too small for the wide layout. In responsive narrow race canvases, PaddockJS measures timing-row entries and the lower-third before adding bottom clearance, so extra race height is reserved only when the banner would cover timing entries. Set it to `false`, or pass `responsiveNarrowLayout: false` to `mountRaceCanvas()` / `mountRaceTelemetryDrawer()`, only when a host intentionally wants the non-responsive stacked behavior.
948
981
 
949
982
  No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries. Narrow hosts are handled internally: side-gutter timing towers become stacked/full-width, embedded timing towers stop behaving like desktop side overlays, and the camera safe area stops reserving a left gutter when the measured timing board is effectively full-width.
950
983
 
984
+ Timing gap display is runtime state on the simulator/controller, not a property of the rendered button. `ui.timingGapMode` only chooses the initial value. After mounting, `setTimingGapMode('interval' | 'leader')` updates all mounted timing towers immediately, `getTimingGapMode()` reports the current value, and `toggleTimingGapMode()` switches to the other supported value. Hiding the manual toggle does not disable those methods; it only removes the package-owned header affordance so a host can provide its own external control.
985
+
951
986
  ## Theme And Sizing Contract
952
987
 
953
988
  ```js
954
989
  theme: {
955
- accentColor: '#e10600',
956
- greenColor: '#14c784',
957
- yellowColor: '#ffd166',
990
+ mode: 'system',
991
+ use: 'trackside',
992
+ tokens: {
993
+ primary: { light: '#c90400', dark: '#e10600' },
994
+ primaryText: '#ffffff',
995
+ pitLane: '#7c3aed',
996
+ },
997
+ themes: {
998
+ trackside: {
999
+ extends: 'default',
1000
+ tokens: {
1001
+ yellowFlag: { dark: '#ffcc00' },
1002
+ },
1003
+ components: {
1004
+ button: {
1005
+ background: 'pitLane',
1006
+ text: 'primaryText',
1007
+ border: 'primary',
1008
+ },
1009
+ },
1010
+ },
1011
+ },
1012
+ componentThemes: {
1013
+ 'race-controls': 'trackside',
1014
+ 'timing-tower': 'selectedTeam',
1015
+ },
1016
+ teamThemes: {
1017
+ ferrari: 'trackside',
1018
+ },
958
1019
  timingTowerMaxWidth: '390px',
959
1020
  raceViewMinHeight: '620px',
960
1021
  }
961
1022
  ```
962
1023
 
963
- These values are applied as package CSS variables:
964
-
965
- - `accentColor` -> `--paddock-accent-color`
966
- - `greenColor` -> `--paddock-green-color`
967
- - `yellowColor` -> `--paddock-yellow-color`
1024
+ Themes may be partial, but resolved themes are always complete. The package `default` theme owns the token schema; named themes in `theme.themes` are derivative packages that can override only those known tokens. Unknown theme tokens and unknown component slots are ignored instead of becoming new CSS variables. If a token is supplied only for `light` or only for `dark`, PaddockJS generates and caches the opposite-mode value in the resolved theme. `theme.use` selects the active named package. `theme.componentThemes` maps `data-paddock-component` names such as `race-controls`, `camera-controls`, `timing-tower`, `race-canvas`, and `race-data-panel` to `default`, `active`, `selectedTeam`, `team`, `team:<id>`, or a named theme package. Component theme keys may also use camelCase aliases such as `raceControls` and `timingTower`; `selectedDriverPanel` targets both selected-driver package surfaces. `theme.teamThemes` maps team ids to selectors for team-aware surfaces; `entries[*].team.theme` feeds the same map, and explicit `theme.teamThemes` values override team metadata when both are supplied. Selected-driver surfaces such as the car/driver overview and race-data panel use the selected team's theme by default when one is available; structural controls keep the active global theme unless explicitly overridden. Theme and driver/team colors are validated before they are written to CSS variables.
1025
+
1026
+ Supported theme tokens are applied as package CSS variables:
1027
+
1028
+ - `primary` -> `--paddock-color-primary`
1029
+ - `primaryText` -> `--paddock-color-primary-text`
1030
+ - `secondary` -> `--paddock-color-secondary`
1031
+ - `secondaryText` -> `--paddock-color-secondary-text`
1032
+ - `surface` -> `--paddock-color-surface`
1033
+ - `surfaceRaised` -> `--paddock-color-surface-raised`
1034
+ - `surfacePanel` -> `--paddock-color-surface-panel`
1035
+ - `text` -> `--paddock-color-text`
1036
+ - `mutedText` -> `--paddock-color-muted-text`
1037
+ - `border` -> `--paddock-color-border`
1038
+ - `success` -> `--paddock-color-success`
1039
+ - `warning` -> `--paddock-color-warning`
1040
+ - `danger` -> `--paddock-color-danger`
1041
+ - `info` -> `--paddock-color-info`
1042
+ - `yellowFlag` -> `--paddock-color-yellow-flag`
1043
+ - `greenFlag` -> `--paddock-color-green-flag`
1044
+ - `redFlag` -> `--paddock-color-red-flag`
1045
+ - `safetyCar` -> `--paddock-color-safety-car`
1046
+ - `drsActive` -> `--paddock-color-drs-active`
1047
+ - `pitLane` -> `--paddock-color-pit-lane`
1048
+ - `track` -> `--paddock-color-track`
1049
+ - `trackEdge` -> `--paddock-color-track-edge`
968
1050
  - `timingTowerMaxWidth` -> `--paddock-timing-tower-max-width`
969
1051
  - `raceViewMinHeight` -> `--paddock-race-view-min-height`
970
1052
 
1053
+ The old `accentColor`, `greenColor`, `yellowColor`, and related `*Color` fields remain compatibility aliases that feed the semantic tokens and legacy CSS variables such as `--paddock-accent-color`.
1054
+
971
1055
  Prefer these fields over host CSS overrides. They are the stable styling surface for reusable embeds.
972
1056
 
973
1057
  ## Race Completion Snapshot
@@ -1024,7 +1108,7 @@ Every built track is automatically divided into three equal sectors. `snapshot.t
1024
1108
 
1025
1109
  Every built track also exposes hidden `snapshot.track.timingLines`. Timing lines are spaced from the track length at an F1-style mini-sector target of roughly `150m..200m`; they are simulation metadata for gap calculation and are not rendered by default.
1026
1110
 
1027
- Every built track also exposes `snapshot.track.pitLane`. The pit lane is deterministic for the track seed and contains:
1111
+ Tracks whose resolved generation options enable pit lanes expose `snapshot.track.pitLane`. The `race` profile includes this geometry; training profiles are pitless by default unless explicitly overridden. When present, the pit lane is deterministic for the track seed and contains:
1028
1112
 
1029
1113
  - `entry`: track distance before the start line, the true track `edgePoint`, an overlapping lane-facing `trackConnectPoint` on the track surface, connector points from the racing surface to the pit lane, and a procedural `roadCenterline` that is tangent to the main track at entry and tangent to the straight pit lane at the pit-lane start.
1030
1114
  - `layout`: the model-owned pit sizing data, including the box-run length, total main-lane length, entry/exit distances relative to start/finish, and entry/exit buffers. The main lane is sized from the configured team/box count instead of using a fixed oversized straight.
@@ -1088,10 +1172,13 @@ Controller methods:
1088
1172
  - `callSafetyCar()`: deploys the safety car.
1089
1173
  - `clearSafetyCar()`: releases the safety car.
1090
1174
  - `toggleSafetyCar()`: switches safety car deployment based on the current snapshot.
1091
- - `setPitIntent(driverId, intent, targetCompound?)`: requests, clears, or updates a pending automatic pit stop and optional target tire.
1175
+ - `setPitIntent(driverId, intent, targetCompound?)`: requests, clears, or updates a pending automatic pit stop and optional target tire. This requires `rules.modules.pitStops.enabled: true` and track pit-lane geometry; `tireStrategy` alone only affects compound choices.
1092
1176
  - `getPitIntent(driverId)`: reads the current pit intent.
1093
1177
  - `getPitTargetCompound(driverId)`: reads the current pit target tire.
1094
1178
  - `getSimulationSpeed()`: returns the active browser playback multiplier from the package-owned simulation-speed control, defaulting to `1`.
1179
+ - `setTimingGapMode(mode)`: sets the timing tower gap mode to `'interval'` or `'leader'` and immediately re-renders mounted timing towers. Invalid values fall back to `'interval'`.
1180
+ - `getTimingGapMode()`: returns the current timing tower gap mode.
1181
+ - `toggleTimingGapMode()`: switches between `'interval'` and `'leader'` and returns the active mode.
1095
1182
  - `servePenalty(penaltyId)`: marks an issued drive-through or stop-go penalty as served.
1096
1183
  - `cancelPenalty(penaltyId)`: cancels a penalty so it no longer affects service, timing, grid, or classification.
1097
1184
  - `getSnapshot()`: returns the latest simulation snapshot.
@@ -1102,15 +1189,16 @@ Composable controllers additionally expose:
1102
1189
  - `mountCameraControls(root)`: renders package-owned camera mode, zoom, and project/radio banner mute controls outside the race canvas.
1103
1190
  - `mountSafetyCarControl(root)`: renders a package-owned safety-car button that binds to the same race-control state as other safety buttons.
1104
1191
  - `mountTimingTower(root)`: renders the timing tower component. The tower includes one hidden race-control status banner slot above the timing rows; `raceControl.mode: 'safety-car'` shows the yellow safety-car status, and `raceControl.mode: 'red-flag'` shows the red red-flag status.
1105
- - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and the top steward message. Camera controls are external by default and render inside the race canvas only when `ui.cameraControls: 'embedded'` is explicitly requested. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTelemetrySectorBanner: true` only when the host intentionally wants the independent sector lower-third in addition to the project/radio banner. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. This is required before `start()`.
1192
+ - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit, responsiveNarrowLayout })`: renders the PixiJS canvas host, optional FPS, start lights, and the top steward message. Camera controls are external by default and render inside the race canvas only when `ui.cameraControls: 'embedded'` is explicitly requested. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTelemetrySectorBanner: true` only when the host intentionally wants the independent sector lower-third in addition to the project/radio banner. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. Responsive narrow timing reveal behavior is enabled by default and can be disabled with `responsiveNarrowLayout: false`. This is required before `start()`.
1106
1193
  - `mountTelemetryPanel(root, { includeOverview })`: renders the package-owned telemetry stack template. The stack is only a composition of detached telemetry surfaces, owns vertical scrolling when its host is shorter than its contents, and includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
1107
1194
  - `mountTelemetryCore(root)`: renders selected-car scalar telemetry only.
1108
1195
  - `mountTelemetrySectors(root)`: renders the live sector progress graph only.
1109
1196
  - `mountTelemetryLapTimes(root)`: renders current, last, and best lap timing only.
1110
1197
  - `mountTelemetrySectorTimes(root)`: renders last and best sector timing only.
1111
- - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail })`: renders a template that combines an external top control row, race canvas, embedded timing tower, the project/radio lower-third, top steward message, safety-car control, and a right-side telemetry drawer. The control row contains camera controls, the `1x..10x` simulation-speed toggle, banner mute, the safety-car button, and the telemetry toggle so those controls do not cover the race view. Pass `raceDataTelemetryDetail: true` when this template should put compact S1/S2/S3 detail in the project lower-third instead of mounting a second sector popup. The drawer embeds the same package-owned telemetry stack used by `mountTelemetryPanel()` and takes layout space from the race window when opened.
1198
+ - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail, responsiveNarrowLayout })`: renders a template that combines an external top control row, race canvas, embedded timing tower, the project/radio lower-third, top steward message, safety-car control, and a right-side telemetry drawer. The control row contains camera controls, the `1x..10x` simulation-speed toggle, banner mute, the safety-car button, and the telemetry toggle so those controls do not cover the race view. Pass `raceDataTelemetryDetail: true` when this template should put compact S1/S2/S3 detail in the project lower-third instead of mounting a second sector popup. The drawer embeds the same package-owned telemetry stack used by `mountTelemetryPanel()` and takes layout space from the race window when opened on wide containers; narrow drawer reveal behavior is enabled by default, stacks the top workbench controls into readable full-width rows, and can be disabled with `responsiveNarrowLayout: false`.
1112
1199
  - `mountCarDriverOverview(root)`: renders the package-owned car/driver overview as a separate component with a Car/Driver toggle, center visual, and linked stat cells from the existing driver/vehicle rating components.
1113
1200
  - `mountRaceDataPanel(root)`: renders the project/race-data lower-third as a separate component for hosts that intentionally want it outside the race canvas.
1201
+ - `querySelector(selector)` / `querySelectorAll(selector)`: search across the mounted package-owned composable roots. These exist for integration tests and advanced host glue; ordinary hosts should prefer explicit controller methods and mounted component roots.
1114
1202
  - `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
1115
1203
 
1116
1204
  Mount component roots before calling `start()`. If a component is not mounted, the runtime skips that UI surface instead of requiring hidden placeholder DOM. Mounted surfaces render a package-owned loading overlay immediately; `start()` removes those overlays after PixiJS, assets, controls, and initial readouts have initialized.