{
 "$comment": "Baseline's hard constraints as structured data. Hand-authored; NOT generated (the compiler's own files are the underscore-prefixed ones: _ds_manifest.json, _ds_bundle.js, _adherence.oxlintrc.json). Every value here was measured from the sources rather than transcribed from prose, where a rule and the code once disagreed, the code won and the prose was corrected. Read this first, then the prose in readme.md for the reasoning. 0.4 reconciles this distribution with the /T fork session: see `amendments`.",
 "system": "Baseline",
 "version": "0.4",
 "updated": "2026-10-20",
 "premise": "A data visualisation practice. Structure in ink; colour only where there is data.",
 "howToUse": {
  "order": [
   "design-rules.json (this file)",
   "components.json (pick a component)",
   "readme.md (reasoning)",
   "<Component>.d.ts (props contract)",
   "<Component>.prompt.md (variants, cautions)",
   "guidelines/*.card.html (specimens)"
  ],
  "authority": "This file states the constraint. readme.md states why. If they ever disagree, measure the source and fix both.",
  "adherence": "_adherence.oxlintrc.json is generated and lints token usage in consuming projects. This file covers what a linter cannot check: budgets, proportions, doctrine.",
  "pitfallsFirst": "Read `pitfalls` before writing a page. Each entry cost a correction round in this project, and none is inferable from the tokens.",
  "notShippedInThisDistribution": {
   "note": "Referenced by this file and readme.md but absent from the 0.4 distribution. Do not assume they exist.",
   "files": [
    "components.json",
    "components/**/*.d.ts",
    "components/**/*.jsx",
    "components/**/*.card.html",
    "guidelines/*.card.html (incl. brand-mark, brand-wordmark)",
    "guidelines/hierarchy/",
    "ui_kits/",
    "templates/",
    "_ds_manifest.json",
    "_adherence.oxlintrc.json",
    "tests/contrast-figures.mjs",
    "tools/build-system-md.mjs"
   ]
  }
 },
 "colour": {
  "law": "Structure is ink. Colour appears only where there is data.",
  "roles": {
   "ground": "#f3f2f2",
   "ink": "#201e1d",
   "accent": "#ec3013",
   "counterSignal": "#2f4b9b"
  },
  "roleCount": 4,
  "required": [
   "Read a semantic token, never a role colour: var(--mark-accent) not var(--color-accent), var(--text-mark) not var(--color-accent). Semantic tokens are what the .on-inverse scope can correct; a role colour cannot be.",
   "A mark colour and a text colour are different tokens even at the same value: --mark-* for shapes and fills, --text-* for glyphs."
  ],
  "accentBudget": {
   "limit": 0.04,
   "unit": "fraction of surface area",
   "note": "An AREA budget, not a count. Count filled controls FIRST when auditing: --control-fill resolves to --color-accent-700, so every filled Button, selected Tag, active SegmentedControl segment and selected chip is a solid accent block, usually the largest on the surface, since a control fill out-measures a chart mark. The name reads as ink; it is not.",
   "authoredVsDeclared": "Some accent is chosen by the author; some is declared by a component's semantics (Delta's fall, LineChart's breakAt, TitleBlock's issue status). Both count against the area budget; only the authored ones are yours to move.",
   "adjacency": "A filled control and an accented mark in the same region read as one emphasis smeared across two. Treat them as one."
  },
  "darkGround": {
   "mechanism": ".on-inverse token scope",
   "count": 1,
   "idempotent": true,
   "note": "Set the scope once at the top of a surface. Every nested component flips from the tokens. The nine components carrying their own `inverse` prop APPLY the same scope rather than swapping colours by hand, so passing it inside a dark surface is redundant but harmless.",
   "trap": "Inside .on-inverse the dark ground is var(--color-bg), NOT var(--color-text), both tokens have already flipped. Reaching for --color-text paints light-on-light.",
   "accentDirection": "brighter, not darker (--mark-accent resolves to --color-accent-500)"
  },
  "alpha": {
   "note": "Permitted in exactly five named roles. 'None' was never true; anything outside this list is a new decision needing a reason on the record.",
   "permitted": [
    {
     "role": "divider ink",
     "value": "--color-divider, ink at 40%",
     "where": "rules that must sit on any ground"
    },
    {
     "role": "disabled state",
     "value": "--state-disabled-opacity, 0.45",
     "where": "Button, IconButton, Field, Menu item"
    },
    {
     "role": "under-grid",
     "value": "--ground-opacity, 10% light / 22% inverse",
     "where": "PlateFrame ground, on its own layer"
    },
    {
     "role": "rail state",
     "value": "0.65 inactive, 0.9 hover",
     "where": "RegisterRail only, furniture, not data"
    },
    {
     "role": "mark separation",
     "value": "0.75 to 0.9",
     "where": "ChordDiagram ribbons, FanChart bands, DotField dots"
    }
   ],
   "forbidden": [
    "alpha on text",
    "alpha as elevation",
    "alpha substituting for a colour that should be named"
   ]
  },
  "fallbackLadder": "colors.css and inverse.css both declare a color-mix() ladder before the oklch() relative-colour line, so engines without relative colour syntax still resolve. Any new derived colour needs both."
 },
 "type": {
  "families": [
   {
    "name": "Public Sans",
    "role": "core",
    "use": "every heading, every line of prose, every figure label, the plate title, the wordmark",
    "weights": [
     400,
     600,
     700,
     800
    ]
   },
   {
    "name": "JetBrains Mono",
    "role": "annotation",
    "use": "dimensions, codes, coordinates, drawing labels, serials",
    "weights": [
     400,
     500,
     700
    ]
   }
  ],
  "familyCount": 2,
  "italic": false,
  "grades": {
   "display": {
    "h1": 132,
    "h2": 96,
    "h3": 60,
    "h4": 34,
    "h5": 24
   },
   "plateTitle": 44,
   "prose": {
    "lead": 21,
    "body": 19,
    "small": 17,
    "caption": 14
   },
   "working": {
    "cell": 16,
    "item": 15,
    "meta": 13,
    "control": 12
   },
   "label": 11
  },
  "minimumSize": 11,
  "required": [
   "Every size is a named grade, in components, UI kits and templates alike. No one-off pixel values anywhere.",
   "Tabular numerals via var(--numeral-tabular) on anything that changes or aligns: stats, axes, table cells, deltas, steps, page numbers. Set with font-variant-numeric, never font-feature-settings.",
   "Wrapping is deliberate: text-wrap balance on headings that wrap, pretty on captions and notes, nowrap on labels and annotation.",
   "Prose caps at --measure-prose (36em, ~68 characters); captions at --measure-caption."
  ],
  "tracking": {
   "rule": "Negative on display, positive on the label grade only.",
   "capsTrackings": {
    "--track-label": "0.12em",
    "--track-label-tight": "0.08em",
    "--track-caps": "0.06em"
   },
   "monoTrackings": {
    "--track-mono-caps": "0.12em",
    "--track-mono": "0.06em"
   },
   "count": 3,
   "note": "Three caps trackings on the core family, not five.",
   "displayTrackings": {
    "--track-h1": "-0.035em",
    "--track-h2": "-0.04em",
    "--track-h3": "-0.03em",
    "--track-h4": "-0.02em"
   },
   "fit": "Fitted to Public Sans: each value keeps the space between letters the system was designed with, measured across the alphabet at the grade's own weight."
  },
  "weight500": {
   "loaded": false,
   "declared": false,
   "reason": "It did nothing the label grade was not already doing."
  },
  "rootFontSize": {
   "setOnRoot": false,
   "reason": "font-size on :root IS the definition of rem. This system ships through styles.css into projects it does not own, setting it would silently rescale every rem-based spacing and sizing value in a consumer (Tailwind is rem-based throughout). The body grade goes on `body`.",
   "onRoot": [
    "font smoothing",
    "text-size-adjust"
   ]
  },
  "wordmark": {
   "cuts": [
    {
     "name": "display",
     "from": "24px",
     "weight": "--weight-black",
     "tracking": "--track-h1",
     "leading": "--leading-h1",
     "use": "project tile, logotype"
    },
    {
     "name": "UI",
     "to": "19px",
     "weight": "--weight-bold",
     "tracking": "--track-wordmark",
     "use": "Masthead"
    }
   ],
   "boundary": 24,
   "note": "Two optical cuts of one word, never two marks. Caps at 19px need loosening; caps at 40px need tightening.",
   "source": "guidelines/brand-wordmark.card.html"
  },
  "headingAccent": "A heading is never accent, with one exception: the single finding per sheet may take it, through Heading accent. Section heads, block heads and the plate title never do. It counts against the 4% budget like any authored accent."
 },
 "spacing": {
  "ladder": [
   8,
   16,
   24,
   32,
   48,
   64,
   96
  ],
  "tokens": [
   "--space-8",
   "--space-16",
   "--space-24",
   "--space-32",
   "--space-48",
   "--space-64",
   "--space-96"
  ],
  "notOnLadder": [
   12
  ],
  "semantics": {
   "--gap-label": "8, label to its content",
   "--gap-within": "16, table rows, list items, swatch to legend",
   "--gap-group": "24, between groups inside one block",
   "--gap-block": "32, block to block; equals the gutter",
   "--gap-caption": "48, figure to its caption block",
   "--page-margin": "64 at desktop; steps down the ladder to 48 below 1100px and 24 below 720px",
   "--gap-section": "96, section to section, the only measure that separates sections"
  },
  "inControl": {
   "below": 8,
   "kinds": [
    "seam",
    "knockout",
    "optical",
    "inset",
    "a11y-hidden"
   ],
   "marker": "/* in-control: <kind> */",
   "note": "Values under 8px are allowed only inside a single component, for one of: seam (gap between chart cells), knockout (padding of a label that masks a line behind it), optical (an alignment offset, including negative margins), inset (a control's own inner offset). a11y-hidden is the visually-hidden pattern (`margin: -1px`). Each kept line carries the marker; an unmarked value under 8px, or a marked value of 8px or more that is off the ladder, is still a violation. The grader reports marked lines as INFO `spacing-in-control`. Between components, and for any card or page spacing, only the ladder applies."
  },
  "specimen": {
   "marker": "/* specimen */",
   "note": "Guideline pages, specimen cards and the Design templates may set type at sizes and tracking that have no token when the value is the subject being shown: display sizes, specimen ramps, role swatches. Each such declaration carries the marker immediately after it, inside the same rule or style attribute; the grader reports marked lines as INFO rather than a violation. Components, cards' component usage and kit pages may not: they use a named grade and a tracking token."
  },
  "rule": "Each measure means exactly one thing. A value off the ladder is a new decision, not a nudge."
 },
 "grid": {
  "columns": 12,
  "gutter": 32,
  "margin": 64,
  "contentWidth": 1232,
  "breakpointColumns": [
   12,
   8,
   4
  ],
  "marginSteps": [
   64,
   48,
   24
  ]
 },
 "structure": {
  "radius": {
   "default": 0,
   "controls": 0,
   "note": "--radius-target is for dots and circular marks only. Controls are square: the pill was tried and reverted, knob included."
  },
  "shadow": "none",
  "cards": false,
  "rules": {
   "--rule-row": "1px, a division within one section",
   "--rule-section": "2px, a section boundary"
  },
  "boxing": "A rule pair, not a card. Bound a band with 2px above and 1px below.",
  "controlHeight": {
   "standard": 46,
   "compact": 40
  },
  "focus": {
   "ring": "--focus-ring 2px",
   "offset": "--focus-offset 2px",
   "colour": "--state-focus (accent)"
  },
  "controls": {
   "segmentedControl": "The selected segment fills with --control-fill (accent 700) and --control-fill-ink, like every filled control, and counts against the accent budget. Changed in 0.4; it was ink.",
   "inkTone": {
    "tokens": "tokens/tone.css · [data-tone=\"ink\"]",
    "components": [
     "SegmentedControlInk",
     "StepIndexInk",
     "IconButtonInk",
     "FilterChipRowInk",
     "RegisterRailInk",
     "RadioInk",
     "SwitchInk",
     "RangeSliderInk",
     "ComboboxInk",
     "MenuInk"
    ],
    "rule": "Same props as the accent control. Spends no accent. Field and Toast keep red (error, alert)."
   }
  },
  "trimMarks": {
   "use": "A bounded figure or map carries four corner crop marks INSTEAD of a ruled border, never both.",
   "geometry": "From RegistrationMark variant=\"crop\" / PlateFrame marks=\"crop\".",
   "offset": "4px from the corner, outside the content box (/* in-control: optical */).",
   "weight": "--rule-row (1px), --mark-quiet."
  }
 },
 "elevation": {
  "mechanism": "Drawer",
  "count": 1,
  "reason": "Baseline forbids shadow, blur, transparency and cards, nothing conventional is left to lift a surface with. Drawer resolves it by not lifting: it butts against a viewport edge behind a 2px ink rule, on opaque ground.",
  "scrim": false,
  "ariaModal": false,
  "ariaModalReason": "The page underneath stays put and stays interactive, so `false` is truthful rather than a shortcut. A scrim would need transparency.",
  "focusReturn": "Drawer stores document.activeElement on open and restores it on unmount.",
  "consequences": [
   "Menu and Combobox are Drawers, not anchored popovers. A filter menu opening a side panel is heavier than a dropdown; the weight is the decision.",
   "Toast does not float and cannot be made to. It is a status row in flow, bounded by 2px above and 1px below, placed where the status belongs, so it never covers what it reports on. Always give it a `code`.",
   "Tooltip does not float either: opaque ink on opaque ground. It is the bubble, not a positioning engine, the caller owns placement and aria-describedby."
  ],
  "inline": {
   "mechanism": "InlinePanel",
   "status": "decided in 0.4; component not yet built",
   "rule": "For the frequent low-stakes menu (filters, view options): anchored in flow, square, opaque, no shadow, no scrim, pushes content rather than covering it. Drawer stays for the heavy case. One elevation doctrine, two weights."
  }
 },
 "iconography": {
  "set": "Lucide",
  "stroke": 1.5,
  "box": 24,
  "colour": "currentColor",
  "prerequisite": {
   "script": "vendor/lucide.min.js",
   "loadBefore": "_ds_bundle.js",
   "reason": "Icon renders an <i data-lucide> placeholder that only lucide.createIcons() replaces. Without the script every icon is a silent 0x0 element with NO console error.",
   "transitiveUsers": [
    "Button",
    "IconButton",
    "Tag",
    "Table",
    "EmptyState",
    "Drawer",
    "Toast",
    "Menu",
    "Field"
   ],
   "rule": "Any page that loads _ds_bundle.js must load it. templates/*/ds-base.js does this for templates."
  },
  "forbidden": [
   "two icon sets",
   "filled and outline together",
   "an icon standing in for a number",
   "emoji",
   "unicode characters as icons"
  ],
  "nonLucide": "The brand mark, and the system's own registration and dimension marks."
 },
 "brandMark": {
  "name": "Register",
  "form": "Four crop corners around a registered centre, the plate frame made into a mark.",
  "grid": 24,
  "strokeWeights": [
   2
  ],
  "strokeWeightCount": 1,
  "holdsAt": 16,
  "holdsReason": "A single 2px section weight: 2 units render at 1.33px at 16px, and there is no hairline element to disappear first. No small-size redraw.",
  "accent": {
   "element": "centre dot",
   "count": 1,
   "note": "The single dot is the mark's whole accent budget. Never fill the corners, never tint the field."
  },
  "lockup": {
   "markBox": "1.2 × the wordmark's type size",
   "markBoxReason": "The artwork is inset 2 units on the 24-unit grid, so visible height = box × 20/24. Sizing to the type size (not the cap height) makes the box 1.2×.",
   "example": "19px word → 23px box",
   "gutter": "two-thirds the box, which lands on the spacing ladder at both scales",
   "gutterValues": {
    "ui": "--space-16",
    "display": "--space-32"
   },
   "alignment": "centres, not baselines, an SVG has no baseline to sit on",
   "clearSpace": "the mark's own width on all four sides, never less"
  },
  "files": {
   "inline": "Drawn inline with currentColor + var(--mark-accent) so it flips and brightens inside .on-inverse with no second artwork.",
   "flat": [
    "assets/logo.svg",
    "assets/logo-inverse.svg"
   ],
   "flatNote": "For <img>, favicons and anything outside the token scope, these cannot resolve currentColor or a custom property, so the inverse file carries the base accent rather than the brightened one."
  },
  "provenance": "Designed in this project, not supplied. The source board states 'the board is the direction, not the mark.' Geometric construction, not illustration; a pictorial mark would be a separate commission.",
  "source": "guidelines/brand-mark.card.html"
 },
 "backgrounds": {
  "ground": "one flat colour",
  "white": "#ffffff for a printed report sheet only",
  "forbidden": [
   "imagery",
   "photography",
   "gradients",
   "noise",
   "blur",
   "frosted panels",
   "protection gradients",
   "capsules"
  ],
  "patterns": {
   "count": 9,
   "built from": "rules and dots only",
   "rule": "A pattern may sit behind a chart mark or an empty zone. It NEVER sits behind text, not a caption, not an axis label, not a title. Text over a pattern needs an opaque ground chip."
  },
  "exceptions": [
   {
    "name": "geographic basemap",
    "where": "Panels only, never sheets or plates.",
    "conditions": [
     "Greyscale raster tiles only, no hue.",
     "The tile source is credited in the figure caption.",
     "Data marks sit above the tiles; the tiles carry no meaning of their own."
    ],
    "note": "The one sanctioned imagery in the system. Everywhere else the land is a flat vector fill."
   }
  ]
 },
 "figures": {
  "axes": "Two edges, never four: 2px bottom, 1px left. The 2px bottom baseline is confirmed in 0.4.",
  "accent": "One marked series or one called-out part per figure.",
  "confidence": "Counted, not modelled. State the method per figure; the apparatus slot may not contradict the rest of the surface.",
  "absence": "Hatched, never shaded. A missing value must not read as a low one.",
  "geography": "Never hand-drawn. Choropleth holds no geometry, pass already-projected paths; projection is a data job.",
  "rangeFrame": "Default value axis: drawn only across the extent the data occupies, marked with its min and max.",
  "endValue": "Every sparkline and every time series carries its closing value at the end of the line.",
  "labelLadder": "At small size a label first moves out onto a leader (Callout), then abbreviates, then, only then, drops. A figure never silently loses the number the reader came for.",
  "overlap": "On DotField, FanChart and ChordDiagram, mark opacity is capped so two stacked marks never read as three, and the caption states that overlap is present.",
  "emptyBins": "An empty bin is drawn as absent or omitted, never floored to a minimum value. On a log axis, a zero bin is omitted and the caption says so.",
  "parts": "Share of a whole is a Ring of three slices at most, or a Waffle of five parts. There is no Pie component.",
  "legendKeys": "A size or tone legend is one row: every label sits on the same line, its key centred beneath it, all keys aligned to one top line. The only sanctioned centring in the system."
 },
 "accessibility": {
  "platformFirst": "Prefer the platform default over a custom rebuild. Checkbox, Radio and Switch are native inputs, visually hidden but focusable and operable; Table is a real table.",
  "contrast": {
   "text": "APCA Lc 75 for body text; Lc 60 for labels, captions, mark numerals and annotation.",
   "marks": "3:1 against their ground"
  },
  "keyboard": "Every interactive component is reachable and operable by keyboard, with a visible focus ring at --focus-ring / --focus-offset.",
  "language": "Every page declares lang."
 },
 "forbidden": {
  "visual": [
   "bluish-purple gradients",
   "emoji cards",
   "rounded containers with a coloured left border only",
   "cards",
   "shadows",
   "rounded corners on controls",
   "italic type"
  ],
  "code": [
   "raw pixel font sizes, use a named grade",
   "raw letter-spacing values, use a tracking token",
   "raw font-weight numerals, use a weight token",
   "font-variant-numeric: tabular-nums literal, use var(--numeral-tabular)",
   "reading a role colour directly in a component",
   "font-size on :root",
   "a value off the spacing ladder",
   "a wrapper painting a pattern behind a component's text, use the component's own ground prop"
  ]
 },
 "voice": {
  "note": "Full treatment with examples in readme.md → CONTENT FUNDAMENTALS.",
  "principles": [
   "Matter-of-fact. State the finding, then the method.",
   "A number is cited, not announced.",
   "Caps belong to the label grade; sentences do not shout.",
   "No emoji anywhere.",
   "Every status carries a code, so it can be cited.",
   "Ranges use \"to\" (2019 to 2026), never a dash; sheet numbers keep the em dash (A—12)."
  ]
 },
 "index": {
  "globalCss": "styles.css",
  "tokens": "tokens/",
  "components": "components/<group>/",
  "componentGroups": [
   "annotation",
   "controls",
   "diagrams",
   "display",
   "feedback",
   "figures",
   "furniture",
   "icons",
   "inputs",
   "layout",
   "navigation",
   "overlay",
   "plate",
   "text"
  ],
  "componentCount": 82,
  "uiKits": [
   "ui_kits/sheet",
   "ui_kits/panel",
   "ui_kits/plate"
  ],
  "templates": [
   "templates/sheet",
   "templates/panel",
   "templates/plate"
  ],
  "guidelines": "guidelines/",
  "assets": "assets/",
  "briefs": [
   "guidelines/aesthetic-brief.md",
   "guidelines/enrichment-brief.md"
  ],
  "generated": [
   "_ds_bundle.js",
   "_ds_manifest.json",
   "_adherence.oxlintrc.json"
  ],
  "retrievalIndex": "components.json",
  "examples": [
   "examples/construction-pipeline",
   "examples/tel-aviv-parks",
   "examples/live-seismicity"
  ]
 },
 "pitfalls": [
  {
   "id": "icons-silent",
   "severity": "high",
   "symptom": "Every icon is invisible; no console error.",
   "cause": "The Lucide UMD script is missing. Icon renders an <i data-lucide> placeholder that only lucide.createIcons() replaces, and it fails to a 0x0 element silently.",
   "fix": "Load vendor/lucide.min.js before _ds_bundle.js. Any page loading the bundle needs it, because any component may pull Icon in transitively."
  },
  {
   "id": "inverse-ground",
   "severity": "high",
   "symptom": "Light text on light ground inside a dark surface.",
   "cause": "Reaching for var(--color-text) as the dark ground inside .on-inverse. Both tokens have already flipped: the dark ground is var(--color-bg) there.",
   "fix": "Inside .on-inverse, --color-bg IS the dark ground and --color-text IS the light ink. Do not invert by hand; the scope did it."
  },
  {
   "id": "control-fill-is-accent",
   "severity": "high",
   "symptom": "A surface blows its 4% accent budget while appearing to spend one mark.",
   "cause": "--control-fill resolves to --color-accent-700. Every filled Button, selected Tag, active segment and selected chip is a solid accent block, usually the largest on the surface. The name reads as ink.",
   "fix": "Count filled controls first when auditing accent. Treat a filled control and an accented mark in the same region as one emphasis."
  },
  {
   "id": "pattern-behind-text",
   "severity": "medium",
   "symptom": "A dotted or hatched ground runs behind axis labels, captions or callouts.",
   "cause": "Wrapping a component in a patterned div. The plot box and its label row are siblings, so a wrapper necessarily paints behind both.",
   "fix": "Use the component's own ground prop (LineChart, PlateFrame), which paints the marks layer only. Text that must sit over a pattern needs an opaque ground chip: background in the ground colour plus padding, each line marked /* in-control: knockout */. Never a box-shadow; shadow is forbidden."
  },
  {
   "id": "axonometric-cannot-shrink",
   "severity": "medium",
   "symptom": "Drawing callouts overwrite a neighbouring table.",
   "cause": "Axonometric reserves a callout column and positions callouts absolutely off its unsquashed geometry, so a squeezed root spills them rather than reflowing.",
   "fix": "Its root is flex:none with a matching min-width. Give it the width it asks for, or let the surrounding layout wrap to one column. minmax(0,1fr) permits collapse to zero, use a real floor."
  },
  {
   "id": "svg-style-stripped",
   "severity": "medium",
   "symptom": "A standalone .svg renders in a UA serif despite naming the families.",
   "cause": "This project's serializer strips <style> from .svg files, so an @import cannot survive there. And an SVG loaded as a separate document, by <img> OR <object>, inherits no @font-face from the embedding page and cannot fetch its own.",
   "fix": "Inline the SVG markup into the HTML page that loads the fonts. For standalone use, install Public Sans and JetBrains Mono locally."
  },
  {
   "id": "stale-bundle",
   "severity": "low",
   "symptom": "A component edit does not appear in its card.",
   "cause": "Cards mount from _ds_bundle.js, which the compiler regenerates at end of turn. Mid-turn the bundle is the previous state.",
   "fix": "Verify component source by reading it; verify rendered output on the next turn. Do not re-edit working source because a stale render disagrees with it."
  }
 ],
 "boilerplate": {
  "$comment": "The minimal correct head for a page that mounts Baseline components. Load order matters: styles.css for tokens, Lucide before the bundle, bundle last.",
  "page": [
   "<link rel=\"stylesheet\" href=\"styles.css\">",
   "<script src=\"vendor/react.production.min.js\"></script>",
   "<script src=\"vendor/react-dom.production.min.js\"></script>",
   "<script src=\"vendor/babel.min.js\"></script>",
   "<script src=\"vendor/lucide.min.js\"></script>",
   "<script src=\"_ds_bundle.js\"></script>"
  ],
  "templates": "A template loads in two steps: ./support.js (the platform runtime, which brings React, ReactDOM and Babel) and templates/<slug>/ds-base.js (styles.css, Lucide, then the bundle). Point ds-base.js `base` at the bound _ds/<folder>. Never add integrity= to a local script: SRI needs a CORS response and blocks the file.",
  "links": "Always define `a` and `a:hover` from the palette, var(--text-mark) and var(--color-accent-900), even with no links yet."
 },
 "surfaces": {
  "panelFrame": {
   "aspect": "16:10",
   "note": "A panel (L6) is a fixed 16:10 frame, scaled to fit the screen. Content blocks are content-sized; one flex region absorbs the slack. No vertical overflow."
  }
 },
 "maps": {
  "interactive": "Leaflet, vendored locally, inside a panel only.",
  "rules": [
   "Basemap follows backgrounds.exceptions[geographic basemap].",
   "Graticule follows the projection; equator and prime meridian are the only emphasised lines.",
   "Data marks: hollow ink outlines; accent on the one mark under discussion only, never on every polygon.",
   "The map canvas sits 8px inside its crop marks.",
   "A filter reframes the view to the selected extent.",
   "Popups and tooltips are square, opaque, no shadow."
  ],
  "note": "Leaflet 1.9.4 is vendored at vendor/leaflet.js and vendor/leaflet.css. Its default chrome (rounded controls, shadows) must be overridden to the system."
 },
 "surfacesOutsideSystem": {
  "note": "Explorations made in the /T session, not system surfaces and not to be cited as precedent.",
  "items": [
   "Baseline T mobile kit",
   "Baseline T Material kit"
  ]
 },
 "amendments": [
  {
   "n": 6,
   "title": "Heading accent permitted once per sheet",
   "see": "type.headingAccent"
  },
  {
   "n": 7,
   "title": "SegmentedControl selection spends accent",
   "see": "structure.controls"
  },
  {
   "n": 8,
   "title": "Label contrast floor is Lc 60",
   "see": "accessibility.contrast"
  },
  {
   "n": 9,
   "title": "Ground chip without shadow",
   "see": "pitfalls[pattern-behind-text]"
  },
  {
   "n": 10,
   "title": "Geographic basemap exception",
   "see": "backgrounds.exceptions"
  },
  {
   "n": 11,
   "title": "Trim marks replace borders; 16:10 panel frame",
   "see": "structure.trimMarks, surfaces.panelFrame"
  },
  {
   "n": 12,
   "title": "Interactive maps",
   "see": "maps"
  },
  {
   "n": 13,
   "title": "The /T fork decisions",
   "items": [
    "Accent budget checked by a dev-time pass, not trusted (not yet built).",
    "Colour/size namespace mix is a lint error (not yet built).",
    "Labels ladder down before they drop.",
    "Overlap capped and declared.",
    "Range frame is the default axis; series carry an end value.",
    "InlinePanel added alongside Drawer (not yet built).",
    "Authoring traps become a preflight; a static component gallery bridges the Figma export (not yet built).",
    "Public Sans is the core family (already amendment 4)."
   ]
  },
  {
   "n": 14,
   "title": "Ink controls",
   "see": "structure.controls.inkTone"
  },
  {
   "n": 15,
   "title": "Legend keys centre under their labels",
   "see": "figures.legendKeys"
  }
 ]
}
