---
name: elt-os-design
description: Design and build ELT Global app screens and evidence-led reports using the companion design.css foundation.
---

# ELT OS — design.md

> The judgment layer for ELT Global, Triple i, Westmount, and Black. Read this file before building. Use the companion **design.css** unchanged for exact values and mechanics.

**Version** 1.1 · **Date** 9 September 2026 · **Source of truth** `eltglobal-os`. Foundation values mirror the repository; this package adds report composition, visualisation, and agent-authoring rules where the product system does not supply them.

---

## Contents

0. [How to use this file](#0-how-to-use-this-file)
1. [Identity](#1-identity)
2. [Reader and task](#2-reader-and-task)
3. [Foundations](#3-foundations)
4. [Theme](#4-theme)
5. [Layout](#5-layout)
6. [Typography](#6-typography)
7. [Components](#7-components)
8. [Data and visualisation](#8-data-and-visualisation)
9. [Writing](#9-writing)
10. [Anti-patterns](#10-anti-patterns)
11. [Review](#11-review)
12. [Runtime snippets](#12-runtime-snippets)
13. [Appendix](#13-appendix)

---

## 0. How to use this file

### 0.1 The two-file contract

- **design.md** decides what to build, what to emphasise, and how to judge the result.
- **design.css** owns exact tokens, component mechanics, responsive rules, and the bounded `elt-` vocabulary. Load it unchanged; do not copy its values into page CSS or invent parallel primitives.

```html
<link rel="stylesheet" href="design.css">
```

When network access exists, load Inter at weights 400, 500, 600, and 700. The stylesheet supplies the fallback stack.

### 0.2 Deliverables

- **App screen:** product UI inside an ELT application. Use the three-layer `.elt-app` shell.
- **Report:** a standalone page intended for reading, sharing, or printing. Use the `.elt-report` shell.

They share the visual foundation but not composition. Reports argue from evidence; app screens help someone complete a task.

### 0.3 Priority order

When requirements compete, protect them in this order:

1. Preserve supplied facts, formulas, units, qualifiers, privacy constraints, and business meaning.
2. Preserve the caller's framework, routes, delivery surface, and established ELT foundation.
3. Make the reader's question, strongest supported answer, and material evidence immediately clear.
4. Establish ELT authorship through typography, alignment, surfaces, and restraint.
5. Choose a composition specific to the task instead of forcing a standard report silhouette.
6. Refine interaction, responsive behavior, and detail without weakening hierarchy or evidence.

### 0.4 Invariants and defaults

An **invariant** protects meaning, accessibility, or system integrity. A **default** is the normal starting point and may be overruled when the task provides a concrete reason.

**Invariants**

- Use `elt-` classes and semantic `--col-*` tokens. Do not hand-write colors, type sizes, radii, shadows, or z-index values.
- Status colors only encode sourced status. Data color never colors prose.
- Length encodings use a shared scale and normally start at zero. Material charts have a semantic data alternative.
- Use semantic landmarks, ordered headings, native controls, visible focus, accessible names, and non-color cues.
- Light and dark must preserve equivalent hierarchy and contrast; print must remain complete.
- The only permitted inline custom properties are listed once in §5.8.

**Defaults**

- Prefer one evidence home per claim, one primary action per view, and one supported recommendation per report.
- Prefer spacing and alignment to borders, neutral data marks to automatic rainbow palettes, and stillness to decorative motion.
- Treat the density limits in §5.6 as warning thresholds, not excuses to shrink type or hide evidence.

### 0.5 Build procedure

1. Name the reader and the job: what must be understood or done in 60 seconds, and what must remain auditable.
2. Inspect all supplied material. When porting a page, inspect its existing layout before flattening it; arrangement is evidence to keep or deliberately overrule.
3. Choose the app or report shell. For reports, select an opening pattern from §2.2 before choosing components.
4. Normalize facts, units, periods, sources, formulas, unknowns, and caveats. Distinguish observation, derivation, projection, recommendation, and causation.
5. Give each claim one evidence home. Choose prose, number, table, comparison, chart, or interactive model by the reader's task.
6. Write claim-led headings and concise explanatory copy. Omit unsupported metadata or recommendations rather than inventing them.
7. Assemble with the public component roots in §7 and the complete examples in §8.7 and §12.
8. Add only the runtime behavior the artifact earns, then run the review in §11.

---

## 1. Identity

### 1.1 Brand scope

ELT OS serves an education group whose software is used by counsellors, faculty, students, and leadership. Four skins share one system:

| Brand | `data-brand` | Role |
|---|---|---|
| ELT Global | omit | Parent brand and default |
| Triple i | `triplei` | Orange-accent sub-brand |
| Westmount | `westmount` | Teal-accent sub-brand |
| Black | `black` | Monochrome skin |

Brand changes brand-scoped actions, links, and active states. It does not change spacing, typography, status meaning, or the identity of an established data series.

### 1.2 Observable character

- Content sits on quiet surfaces over a subtly distinct page plane. App sections separate through a narrow gap; reports use larger editorial rhythm.
- Routine boundaries are hairlines. Sections do not acquire borders merely to look contained.
- Inter carries a restrained ink hierarchy: strong headings and values, readable body text, quieter metadata.
- Radii are controlled and consistent. Full pills are reserved for content that is guaranteed to stay on one line.
- Brand color identifies primary action, navigation state, and links. It is not decoration.
- Shadows belong to overlays, sticky chrome, and the few components that require elevation; ordinary cards do not float.
- Motion is short and functional. Dense evidence remains still.

### 1.3 Brand marks

Use the exact assets in §13.2. Do not recolor the wordmark, distort either mark, add effects, or place it on a gradient. Preserve clear space equal to the height of the “E.” Reports use the wordmark once in the footer; applications use it in the sidebar brand slot. Use the inverted mark on dark surfaces.

---

## 2. Reader and task

### 2.1 Two reading speeds

Design reports and dashboards for both:

- **Executive path:** identity, title, headings, decisive values, captions, and conclusion communicate the answer quickly.
- **Audit path:** exact tables, assumptions, methods, caveats, and sources preserve the record without dominating the first read.

The executive path must not require the audit path; the audit path must not fight the executive path.

### 2.2 Report composition

The report shell is stable; its argument is not. Every report includes ELT identity, one descriptive `h1`, a clear evidence path, sources, and a footer. Include metadata, a summary, navigation, KPIs, a recommendation, or a calculator only when the material earns them.

Choose the opening by the reader's job:

| Job | Opening pattern |
|---|---|
| Decide between supported actions | Recommendation and decisive basis are co-primary |
| Compare alternatives | Put alternatives on the same visual basis |
| Understand a trend or benchmark | Lead with the relationship, threshold, or exception |
| Test assumptions | Make the working calculator or model the focal evidence |
| Understand a state without a supported decision | Lead with the strongest supported finding, limit, or unresolved question |

The first viewport is the argument, not ceremony. It should establish identity, the reader's question, and the strongest supported evidence. Keep preparation, audience, and document-state metadata together and do not repeat it in a preamble.

Every section answers a new reader question. Its usual rhythm is claim → evidence → interpretation → caveat. A later table may preserve exact lookup, but another summary, chart, or conclusion must not repeat the same answer at equal prominence.

### 2.3 App screen contract

```text
.elt-app
  .elt-app__frame
    .elt-sidebar
    .elt-page
      .elt-actionbar
      .elt-tabbar                 optional
      .elt-page__scroll           primary scroller
        .elt-section
```

Every list and table supports loading, empty, error, and data states. Loading uses stable skeleton geometry; errors explain recovery; empty states offer one useful action when one exists.

### 2.4 Observable decisions

Write rules that can be verified in the rendered artifact. Replace “keep it clean” with a visible outcome such as “the evidence table owns the full evidence width and numeric headers align with their values.”

---

## 3. Foundations

Exact values live in design.css. This section defines how to choose them.

### 3.1 Color roles

- **Page and surfaces:** use page background, primary surface, secondary surface, and stroke tokens according to depth. Do not reproduce their color values.
- **Ink:** primary for headings and decisive values, secondary for reading copy, tertiary for labels and metadata, and brand ink for links or active chrome.
- **Status:** success, alert, and destructive roles require a real state and a non-color cue.
- **Data:** start with neutral or brand emphasis. Use multiple chart slots only when stable series identity improves comprehension.
- **Primitive colors:** `--prim-*` support the stylesheet and are not a page-authoring palette. Reach for one only when no semantic role exists and record why in a code comment.

### 3.2 Type roles

Use the published `.elt-display-*`, `.elt-text-*`, and `.elt-caption` roles rather than choosing sizes numerically. Reserve display roles for a single page-defining statement or exceptional value. Use text roles for headings, body, controls, tables, and metadata according to hierarchy. Equivalent peers always share role, weight, line-height, and numeric treatment.

Available weight modifiers are `.is-regular`, `.is-medium`, `.is-semibold`, and `.is-bold`. Body copy is regular; headings and values normally use semibold; bold is scarce.

### 3.3 Spacing and rhythm

Use `--pad-*` inside components and the `--space-*` roles between content groups, figures, and sections. Give every gap one owner; children inside a stack or grid do not add competing margins. Close relationships use close spacing, new groups use visibly larger spacing, and chapter turns use the largest rhythm only when the content earns it.

### 3.4 Shape, elevation, motion, and layers

Use the radius, shadow, motion, and z-index tokens already applied by components. Do not assign an overlay's layer manually. Full radius promises one line. Add motion only to explain a state change, preserve continuity, or confirm an action; reduced-motion behavior is supplied by design.css.

---

## 4. Theme

### 4.1 Mechanics

The page follows the operating system by default through `color-scheme`. `data-theme="light"` or `data-theme="dark"` forces a mode; the repository's `.dark` convention is also supported. Set `data-brand` for a sub-brand. Radius themes belong to applications, not reports.

Use a local `.elt-scheme-dark` or `.elt-scheme-light` only for a meaningful contrast field, at most once per page. Do not invert or desaturate a finished light design.

### 4.2 User control

- **Applications:** use the host product's established theme control. If none exists and user choice is required, use the toggle and persistence behavior in §12.
- **Standalone reports:** follow system preference by default. Include the visible toolbar toggle only when the delivery context benefits from explicit control.
- **Print:** force a complete light presentation and reveal printable evidence.

Both modes must preserve hierarchy, meaning, focus, and contrast. Layout, spacing, series identity, and status meaning do not change with theme.

---

## 5. Layout

### 5.1 Report shell

```html
<body>
  <div class="elt-report__progress"></div>
  <div class="elt-report__toolbar">…</div>
  <main class="elt-report">
    <article class="elt-report__paper">…</article>
  </main>
</body>
```

Use the default report width for reading. Use `.is-wide` only when a dashboard grid, comparison, or dense matrix needs the evidence width; use `.is-narrow` with `body.is-flat` only for an intentionally flat document. Prose stays in `.elt-prose`; tables, charts, comparisons, and tools may use the full evidence width. An aside may hold navigation or key context, never primary evidence.

### 5.2 App shell and scroll ownership

The app shell fills the viewport. Fixed chrome does not scroll. `.elt-page__scroll` is the default scrolling owner and contains `.elt-section` surfaces. In row mode, full-height panes remain fixed and the long pane uses `.elt-section__body.is-scroll`; below the desktop breakpoint, panes stack and scrolling returns to `.elt-page__scroll`. Modals and drawers scroll only their body.

Inside a section, use `.elt-section__header`, `.elt-section__body`, and optional `.elt-section__footer`. A table uses `.elt-table-wrap.is-inset` to avoid a second boundary. On narrow screens, the sidebar becomes an off-canvas drawer and its menu trigger stays in the visible action bar.

### 5.3 Composition primitives

Use `.elt-stack` for unlike blocks in a vertical sequence, `.elt-row` for one-line relationships, `.elt-cluster` for wrapping controls or tags, `.elt-grid` for true peers, and `.elt-two-col` for prose beside one supporting figure. Tables, time series, and dense evidence take the full evidence width. A comparison uses peer geometry, not a thumbnail-plus-prose arrangement.

Choose a column count from the minimum readable width of the content, not the number of items. If only one readable column fits, use a stack.

### 5.4 Responsive behavior

Reason in three tiers: phone, tablet, and desktop. Verify representative widths at 390, 834, and 1440 pixels. Let the published media and container rules perform the reflow; page-authored breakpoints are a last resort. Recompose before shrinking text, clipping labels, or hiding evidence. Grid and flex children need `min-width: 0`; the page itself must not scroll sideways.

### 5.5 Choosing the evidence container

- Headline numbers use a KPI grid only when they are true peers and needed for the first read.
- App statistics above a table use a section row of individually bounded KPI cards.
- Cards and option blocks use a grid only when they have equal conceptual weight.
- A supporting image may use `.elt-two-col.is-media`; two items being compared use equal comparison geometry.
- Measurements shared by two comparison panes belong in one spanning stat strip.
- Full-height list/detail or rail/content layouts use row-mode app sections.

### 5.6 Density defaults

Density limits are warning thresholds. A report normally has one dominant opening, no repeated summary, no more than one callout per section, and only the figures needed to advance the argument. An app view normally keeps one primary action, a small set of filters and tabs, one overlay layer, and tables whose columns and page size remain scannable.

When a threshold is exceeded, first combine, prioritize, reflow, disclose, paginate, or split the task. Never respond by shrinking type, creating nested scrollers, or hiding decision-relevant evidence.

### 5.7 Stacking order

Use the provided tooltip, toast, popover, modal, drawer, backdrop, and sticky classes; each owns its layer. Do not write a z-index in page code.

### 5.8 Permitted inline parameters

These twelve custom properties are the complete allowlist for data or geometry CSS cannot know:

| Variables | Purpose |
|---|---|
| `--v`, `--a`, `--b`, `--t` | Values, before/after positions, and targets |
| `--stops` | Donut stop list |
| `--h`, `--size`, `--cols` | Chart height, donut size, and heatmap columns |
| `--rail-width`, `--sticky-top`, `--table-max-h` | Context-dependent layout geometry |
| `--progress` | Reading progress |

Runtime-positioned portalled overlays may additionally receive computed `left` and `top`. Modifier-controlled values such as grid gaps, grid columns, and series slots are classes, not inline parameters.

### 5.9 Grid-track authoring

When a custom visual needs explicit tracks, define them on one parent so every row shares the same label, plot, value, and annotation lanes. Do not let repeated rows resolve their widths independently.

---

## 6. Typography

### 6.1 Hierarchy

Reports use one page-defining title, claim-led section headings, short orientation copy, readable body text, and quiet evidence metadata. Applications use compact headings and labels appropriate to controls and dense tables. Do not promote app chrome to report-display scale or compress report prose to UI density.

### 6.2 Rules

- Use Inter and the published type roles. Do not choose a size or numeric weight ad hoc.
- Write sentence-case headings that state what happened, what changes, or what the reader must decide.
- Keep reading prose near 60–68 characters per line. Rewrite or reflow before shrinking.
- Equivalent peers use identical roles. Align mixed-size numbers on their baseline and use tabular numerals for columns, not oversized hero figures.
- Keep emphasis scarce. Avoid decorative all-caps labels, gratuitous italics, and type-size changes used only to make content fit.
- Inspect important line breaks; fix stranded words by improving copy or measure.

---

## 7. Components

The stylesheet is the complete implementation. This section names the public component roots and the decision each family serves. Use the documented root and its existing `__part` and `.is-*` structure; do not invent synonyms or restyle a component from page CSS.

### 7.1 Surfaces and editorial blocks

Use `.elt-section` for app content regions, `.elt-card` for a genuinely bounded peer object, `.elt-panel` for subordinate grouping, `.elt-callout` for a material caveat, `.elt-finding` for one evidence-led finding, `.elt-option` for alternatives, `.elt-rec` for a supported recommendation, `.elt-details` for secondary disclosure, and `.elt-tldr` only when a distinct executive summary is necessary.

Do not nest bounded surfaces or turn every section into a card. If spacing and headings communicate the group, use plain content.

### 7.2 Data display

Use `.elt-hero` for one page-defining value, `.elt-kpi-grid` and `.elt-kpi` for a small peer set, `.elt-stat-strip` for aligned measurements, `.elt-table-wrap` with `.elt-table` for exact lookup, `.elt-kv` for short key/value facts, and progress, meter, badge, chip, or tag components according to meaning. Measurements are not badges; ordinary metadata is not a pill.

### 7.3 Charts

Use `.elt-chart` as the semantic figure frame. Available chart families include `.elt-barlist`, `.elt-cols`, `.elt-split`, `.elt-donut`, `.elt-heatmap`, `.elt-dumbbell`, and the SVG roles consumed by `eltChart`. Choose among them through §8, not by visual variety.

### 7.4 Controls and navigation

Use `.elt-btn`, `.elt-iconbtn`, `.elt-field`, `.elt-input`, `.elt-select`, `.elt-textarea`, `.elt-checkbox`, `.elt-radio`, `.elt-toggle`, `.elt-segmented`, and `.elt-filterbar` for controls. Use `.elt-actionbar`, `.elt-tabbar`, `.elt-breadcrumb`, `.elt-sidenav`, and `.elt-pagination` for navigation. Preserve native semantics, labels, focus, disabled states, and destructive confirmation.

### 7.5 Overlays and feedback

Use `.elt-backdrop`, `.elt-modal`, `.elt-drawer`, `.elt-popover`, `.elt-menu`, `.elt-toast`, `.elt-alert`, `.elt-empty`, and `.elt-skeleton`. Overlay chrome stays fixed while its body scrolls. Tooltips enhance accessible evidence but never contain the only copy of a material value.

### 7.6 Composition and utilities

Use `.elt-stack`, `.elt-row`, `.elt-cluster`, `.elt-grid`, `.elt-two-col`, and the small `elt-` utilities already present in design.css. Icons use `.elt-icon` with Tabler by default or Feather when the source specifies it. Brand assets use `.elt-logo` and `.elt-brandmark`.

The recipes in §8.7 and §12 are the canonical structural examples. If a needed composition has no public root, use semantic HTML plus a narrowly named page-specific class that consumes only semantic tokens; do not modify a published component selector.

---

## 8. Data and visualisation

### 8.1 Choose evidence by task

Give every material claim one primary evidence home.

| Reader task | Preferred form |
|---|---|
| Understand one decisive value | Prose, hero value, or KPI |
| Look up exact records | Semantic table |
| Compare concise qualitative differences | Aligned comparison |
| Compare magnitude or rank | Bars on one shared scale |
| Understand change over time | Line or aligned time series |
| Understand composition | Split bar; donut only for a very small number of clearly different parts |
| Judge movement from before to after | Dumbbell or aligned positions |
| Find a threshold, range, or target | Position relative to a visible boundary |
| Inspect two dimensions | Heatmap when both axes are meaningful |

Use prose for one conclusion and a table for precise lookup. Add a chart only when spatial encoding makes a relationship materially faster to understand. Do not draw a chart merely because values exist.

### 8.2 Honest geometry

Map magnitude to position or length on a common scale, time to horizontal order, composition to proportion, thresholds to distance from a boundary, and processes to connection and sequence. Bars normally start at zero. When denominators differ, choose count or rate explicitly and show the relevant base. Never allow a row's label or annotation to change its plot width.

### 8.3 Color jobs

Start in monochrome or neutral steps. Use brand emphasis for the focal series or selected state. Use multiple chart slots only when stable entity identity improves comprehension; preserve that identity across filters and views. Use sequential or diverging ramps only for ordered values with a stated midpoint. Status colors encode sourced status, never generic importance or favorability.

Pair color with direct labels, position, shape, pattern, or text. Tail categories may become “Other” only when the grouping rule is stated. Never generate a fifth hue to avoid editing the comparison.

### 8.4 Labels and context

State the finding in the title, the measure and period near the chart, and the basis or caveat in the caption. Prefer direct labels to legends; use a legend when two or more series cannot be labelled directly. Show units, population, denominator, precision, and comparison basis close to the evidence they qualify. Select labels that help the reader notice the relationship rather than numbering every mark.

### 8.5 Interaction

Interaction must reduce work or expose evidence. Tooltips, crosshairs, legend isolation, filtering, and sorting are earned by the task, not automatic decoration. Material values remain available without hover and by keyboard. During refresh, preserve stable geometry and the last valid evidence instead of flashing an empty chart.

Tables with meaningful lookup may sort; filters sit above everything they scope and declare the active subset. Insert untrusted labels with `textContent`, never concatenated `innerHTML`.

### 8.6 Accessibility and themes

Every material chart has a concise text alternative or semantic table containing the values needed to understand its claim; the alternative need not be a visible duplicate when direct labels and prose already provide a complete equivalent. SVG charts use an accessible name. Check marks, labels, focus, and non-color cues in both themes, print, forced colors, and reduced motion.

### 8.7 Recipes

Every recipe below is complete: copy it, replace the values, keep the classes.

**KPI row with a sparkline**

```html
<div class="elt-kpi-grid cols-4">
  <div class="elt-kpi">
    <div class="elt-kpi__main">
      <div class="elt-kpi__label">Active students</div>
      <div class="elt-kpi__row">
        <div class="elt-kpi__value">1,687</div>
        <div class="elt-kpi__delta is-up">+4.2% <small>vs last month</small></div>
      </div>
      <div class="elt-kpi__spark" id="spark-students"></div>
    </div>
  </div>
  <!-- three more tiles -->
</div>
<script>eltChart.spark(document.getElementById('spark-students'), [1420,1455,1490,1502,1531,1560,1588,1601,1612,1640,1655,1687]);</script>
```

**Bar list (categories, one series)** — value drives `--v` as a percentage of the maximum.

```html
<figure class="elt-chart">
  <div class="elt-chart__head"><div><div class="elt-chart__title">Calicut and Kochi enrol two-thirds of all students</div><div class="elt-chart__sub">Enrolments by centre · Jul 2026</div></div></div>
  <div class="elt-barlist is-emphasis">
    <div class="elt-barlist__row is-emph"><div class="elt-barlist__label">Calicut</div><div class="elt-barlist__track"><div class="elt-barlist__bar" style="--v:100%"></div></div><div class="elt-barlist__value">612</div></div>
    <div class="elt-barlist__row is-emph"><div class="elt-barlist__label">Kochi</div><div class="elt-barlist__track"><div class="elt-barlist__bar" style="--v:78%"></div></div><div class="elt-barlist__value">478</div></div>
    <div class="elt-barlist__row"><div class="elt-barlist__label">Trivandrum</div><div class="elt-barlist__track"><div class="elt-barlist__bar" style="--v:41%"></div></div><div class="elt-barlist__value">251</div></div>
    <div class="elt-barlist__row"><div class="elt-barlist__label">Thrissur</div><div class="elt-barlist__track"><div class="elt-barlist__bar" style="--v:33%"></div></div><div class="elt-barlist__value">204</div></div>
    <div class="elt-barlist__row is-muted"><div class="elt-barlist__label">Other (4)</div><div class="elt-barlist__track"><div class="elt-barlist__bar is-deemph" style="--v:23%"></div></div><div class="elt-barlist__value">142</div></div>
  </div>
  <figcaption class="elt-chart__caption">Live enrolment records. "Other" folds four centres under 60 students each.</figcaption>
</figure>
```

Variants: `.is-ordinal` for a funnel (rows in stage order; `.is-emphasis` removed); `.is-compare` with two `__bar`s per row (`.is-b` for the second); `.is-diverging` with two `__half`s (`.is-neg` / `.is-pos`) per track.

**Columns (CSS, grouped)**

```html
<figure class="elt-chart">
  <div class="elt-chart__head"><div><div class="elt-chart__title">Pass rate rose in every batch after the revision week</div><div class="elt-chart__sub">Pass rate by batch, before vs after</div></div></div>
  <div class="elt-legend"><span class="elt-legend__key"><i class="elt-legend__swatch is-1"></i>Before</span><span class="elt-legend__key"><i class="elt-legend__swatch is-2"></i>After</span></div>
  <div class="elt-cols has-grid" style="--h:220px">
    <div class="elt-cols__col"><div class="elt-cols__group"><div class="elt-cols__bar s1" style="--v:62%"></div><div class="elt-cols__bar s2" style="--v:78%"></div></div><div class="elt-cols__label">Batch A</div></div>
    <div class="elt-cols__col"><div class="elt-cols__group"><div class="elt-cols__bar s1" style="--v:55%"></div><div class="elt-cols__bar s2" style="--v:71%"></div></div><div class="elt-cols__label">Batch B</div></div>
    <div class="elt-cols__col"><div class="elt-cols__group"><div class="elt-cols__bar s1" style="--v:70%"></div><div class="elt-cols__bar s2" style="--v:84%"></div></div><div class="elt-cols__label">Batch C</div></div>
  </div>
  <figcaption class="elt-chart__caption">Internal assessments, n = 312. Percentages of the batch.</figcaption>
</figure>
```

For a single series, drop `__group` and put the `__bar` directly in `__col`; add `.is-emph` to the column the story is about and `.is-emphasis` to `.elt-cols`. For stacks, use `.elt-cols__bar.is-stack` with `<i class="s1" style="--v:50">` children.

**Line chart (SVG helper, multi-series, crosshair tooltip, table twin)**

```html
<figure class="elt-chart" id="cost-chart">
  <div class="elt-chart__head">
    <div><div class="elt-chart__title">MongoDB cost is rising and volatile; PostgreSQL is flat</div><div class="elt-chart__sub">Monthly database cost, USD · Dec 2024–Jun 2026</div></div>
    <div class="elt-chart__tools"><div class="elt-segmented"><button class="elt-segmented__item is-active" data-elt-view="chart">Chart</button><button class="elt-segmented__item" data-elt-view="table">Table</button></div></div>
  </div>
  <div class="elt-chart__body"></div>
  <figcaption class="elt-chart__caption">Actual invoiced amounts. MongoDB peaked at <b>$511</b> in Sep 2025 during a one-off reporting build.</figcaption>
</figure>
<script>
eltChart.line(document.getElementById('cost-chart'), {
  labels: ['Dec 24','Mar 25','Jun 25','Sep 25','Dec 25','Mar 26','Jun 26'],
  series: [
    { name: 'MongoDB (Atlas)',   slot: 2, values: [35, 63, 78, 229, 511, 192, 447] },
    { name: 'PostgreSQL (Aiven)', slot: 1, values: [null, null, 40, 118, 121, 123, null] }
  ],
  format: v => '$' + v.toLocaleString(),
  yMax: 520, ticks: 5, endLabels: true,
  annotations: [{ x: 4, series: 0, text: 'M40 upgrade' }]
});
</script>
```

The helper (§12.4) writes the SVG with `.elt-svg-*` classes, the legend, the crosshair and tooltip, the table twin, and the aria label. `eltChart.columns(el, { labels, series, stacked, format })` does the same for columns when the CSS version is not enough (many periods, exact ticks).

**Split bar (part-to-whole)**

```html
<div class="elt-split">
  <div class="elt-split__seg is-1" style="--v:45"><span>45% on PostgreSQL</span></div>
  <div class="elt-split__seg is-other" style="--v:55"><span>55% still on MongoDB</span></div>
</div>
<div class="elt-legend"><span class="elt-legend__key"><i class="elt-legend__swatch is-1"></i>Moved and live</span><span class="elt-legend__key"><i class="elt-legend__swatch is-other"></i>Still migrating</span></div>
```

**Heatmap** — `--v` in [0, 1]; add `.is-hi` above 0.55 so the label ink flips.

```html
<div class="elt-heatmap" style="--cols:7">
  <div class="elt-heatmap__corner"></div>
  <div class="elt-heatmap__colhead">Mon</div>…<div class="elt-heatmap__colhead">Sun</div>
  <div class="elt-heatmap__rowhead">09:00</div>
  <div class="elt-heatmap__cell" style="--v:0.2">12</div>…<div class="elt-heatmap__cell is-hi" style="--v:0.9">54</div>
  …
</div>
<div class="elt-scale"><span>0</span><div class="elt-scale__bar"></div><span>60 sessions</span></div>
```

**Comparison matrix (table with verdict cells)**

```html
<div class="elt-table-wrap"><table class="elt-table is-fit">
  <thead><tr><th>Requirement</th><th class="is-center">Option A</th><th class="is-center">Option B</th><th class="is-center">Option C</th></tr></thead>
  <tbody>
    <tr><td>Works offline</td><td class="is-center is-good">Yes</td><td class="is-center is-bad">No</td><td class="is-center is-warn">Partial</td></tr>
    <tr><td>Monthly cost</td><td class="is-center is-num">$118</td><td class="is-center is-num">$447</td><td class="is-center is-num">$260</td></tr>
  </tbody>
</table></div>
```

**Meter, dumbbell, donut, waffle**

```html
<div class="elt-meter is-warn"><div class="elt-meter__fill" style="--v:82%"></div><div class="elt-meter__target" style="--t:75%"></div></div>
<div class="elt-meter-scale"><span>0</span><span>Budget used · 82% of ₹4.8 lakh</span><span>100%</span></div>

<div class="elt-dumbbell">
  <div class="elt-dumbbell__row"><div class="elt-dumbbell__label">Batch A</div><div class="elt-dumbbell__track" style="--a:62%;--b:78%"><div class="elt-dumbbell__link"></div><div class="elt-dumbbell__pt is-a"></div><div class="elt-dumbbell__pt is-b"></div></div><div class="elt-dumbbell__value">62 → 78 <small>+16</small></div></div>
</div>

<div class="elt-donut-row">
  <div class="elt-donut-wrap" style="--size:120px"><div class="elt-donut" style="--stops: var(--chart-1) 0 45%, var(--chart-other) 45% 100%"></div><div class="elt-donut__center"><b>45%</b><small>migrated</small></div></div>
  <div class="elt-legend">…</div>
</div>

<div class="elt-waffle"><i class="is-on"></i>…(30 × .is-on, then 70 × plain)…</div>
```

### 8.8 Before you ship a figure

- The form answers a reader question faster than prose or a table alone.
- Geometry uses a shared, stated basis and does not exaggerate the difference.
- Units, period, population, denominator, and material caveat are present.
- Color has a named job, retains its identity, and is not the only cue.
- Labels remain legible and unclipped; interaction is keyboard-accessible and never gates values.
- A concise text alternative or semantic table preserves the material evidence.
- The figure works in light, dark, print, forced colors, reduced motion, and narrow layouts.

---

## 9. Writing

- Lead with the strongest supported answer, implication, or unresolved question. A title names what happened or what must be decided, not the document genre.
- Preserve every qualifier, population, period, unit, comparison basis, condition, and uncertainty that changes meaning.
- Keep executive-path language plain enough for the least specialized named stakeholder. Preserve exact technical vocabulary in the audit path and define unfamiliar terms once.
- Section headings state the claim or reader question. Paragraphs carry one idea each. Bold only the nouns and numbers that materially help a skim.
- Numbers include appropriate units and periods; comparisons name their base; estimates use only defensible precision.
- Distinguish measured results, derived values, projections, recommendations, and causation. Never invent ownership, urgency, approval, confidence, or a call to action.
- Put caveats next to the evidence they qualify. Describe the chosen method and its material limits, not an execution diary.
- Use active, direct language without hype, exclamation marks, decorative emoji, or internal design vocabulary.
- Button labels are verb phrases. Empty states name what is missing, explain recovery, and offer one useful action when one exists.
- End with the supported decision, implication, next action, or open question. Sources and the footer follow quietly.

---

## 10. Anti-patterns

Reject these before shipping:

- A fixed report template that forces a kicker, summary, KPI row, card grid, and recommendation onto material that does not need them.
- Several summaries, charts, cards, or conclusions giving the same claim equal prominence.
- Sections without a new reader question, or figures added only to satisfy a quota.
- Card-in-card composition, bordered sections, decorative pills, ornamental shadows, gradients, glows, textures, or color fields used to repair weak hierarchy.
- A generic centered hero followed by equal cards, or materially unequal findings forced into equal cells.
- Evidence compressed beside an empty rail, a table narrowed to preserve adjacent prose, or repeated chart rows with independently resolved tracks.
- Body copy made tiny or faint to fit, arbitrary type sizes, inconsistent peer roles, or important line breaks left accidental.
- Status color used as decoration, prose colored by series, automatic rainbow categories, or color without a non-color cue.
- Cropped bar baselines, dual axes, misleading denominators, fake precision, tooltip-only values, or charts that repeat a table without revealing a relationship.
- Nested scrollers, hidden page overflow, inaccessible controls, invented classes, hand-written visual values, foreign icon systems, or untrusted `innerHTML`.
- Decorative motion, auto-playing effects, or reveal animations that delay reading.

When a block needs repeated local patches, stop and re-derive it from the reader's task. The correct fix is often a different structure, not another modifier.

---

## 11. Review

Inspect the rendered first viewport, full page, both themes, print, keyboard path, and representative widths. Fix the highest-impact systemic defect, render again, and repeat.

### 11.1 First read and language

- ELT authorship, the reader's question, and the central evidence are clear in the first viewport.
- Headings and captions communicate the argument without requiring body copy.
- Claims preserve units, periods, qualifiers, and uncertainty; unsupported metadata and recommendations are absent.
- Each section advances a new question and each material claim has one evidence home.

### 11.2 Composition and typography

- One object or relationship is dominant at each reading moment; supporting content is quieter.
- Grids contain true peers, tables and dense evidence receive enough width, and empty space has a compositional purpose.
- Type roles, baselines, measures, and gap ownership are consistent. Nothing was shrunk merely to fit.
- Surfaces, borders, color, icons, and motion each communicate meaning or affordance; removable decoration is gone.

### 11.3 Evidence and interaction

- Geometry, scales, denominators, precision, labels, and caveats honestly support the claim.
- Tables use semantic structure and align numeric headers with values.
- Filters declare their scope and subset; material values do not require hover.
- Loading, empty, error, and data states preserve task continuity in applications.

### 11.4 Access and reflow

- Landmarks, one `h1`, heading order, focus, labels, accessible names, and text alternatives are sound.
- Light and dark preserve equivalent meaning; print contains the full record.
- At 390, 834, and 1440 pixels the page recomposes without horizontal page overflow, clipped labels, character-level wrapping, or unreachable controls.
- Scroll ownership is unambiguous and overlays remain usable without creating a third interaction layer.

---

## 12. Runtime snippets

Plain scripts, no framework, no network. Paste them as shown; do not rewrite them. Together they give every page: a flash-free theme with a persistent toggle, a reading-progress hairline, sticky-nav highlighting, chart/table switching, legend isolation, printable details, sortable tables, and the SVG chart helper that renders lines and columns with the right classes, tooltips, crosshair, legend, table twin, and aria label.

### 12.1 Theme bootstrap — in `<head>`, before the stylesheet

```html
<script>
(function () {
  try {
    var saved = localStorage.getItem('elt-theme');
    if (saved === 'light' || saved === 'dark') document.documentElement.setAttribute('data-theme', saved);
  } catch (e) {}
})();
</script>
<link rel="stylesheet" href="design.css">
```

With no saved choice the page follows the operating system through `color-scheme: light dark`.

### 12.2 Optional report toolbar — first children of `<body>`

```html
<div class="elt-report__progress" aria-hidden="true"></div>
<div class="elt-report__toolbar" role="toolbar" aria-label="Page tools">
  <button class="elt-iconbtn elt-theme-toggle" type="button" data-elt-theme-toggle aria-label="Switch between light and dark">
    <span class="elt-theme-toggle__moon"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"/></svg></span>
    <span class="elt-theme-toggle__sun"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"/><path d="M6.343 17.657l-1.414 1.414"/><path d="M6.343 6.343l-1.414 -1.414"/><path d="M17.657 6.343l1.414 -1.414"/><path d="M17.657 17.657l1.414 1.414"/><path d="M4 12h-2"/><path d="M12 4v-2"/><path d="M20 12h2"/><path d="M12 20v2"/></svg></span>
  </button>
  <button class="elt-iconbtn" type="button" onclick="window.print()" aria-label="Print or save as PDF">
    <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 17h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-14a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h2"/><path d="M17 9v-4a2 2 0 0 0 -2 -2h-6a2 2 0 0 0 -2 2v4"/><path d="M7 13m0 2a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2z"/></svg>
  </button>
</div>
```

In an app shell, place the same `.elt-theme-toggle` button in `.elt-sidebar__footer` or `.elt-actionbar__actions` and omit the progress bar.

### 12.3 Page runtime — end of `<body>`

```html
<script>
(function () {
  var root = document.documentElement;
  function mode() {
    var t = root.getAttribute('data-theme');
    if (t === 'light' || t === 'dark') return t;
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }
  // Theme toggle — persists under localStorage 'elt-theme'
  document.querySelectorAll('[data-elt-theme-toggle]').forEach(function (btn) {
    btn.addEventListener('click', function () {
      var next = mode() === 'dark' ? 'light' : 'dark';
      root.setAttribute('data-theme', next);
      try { localStorage.setItem('elt-theme', next); } catch (e) {}
      document.dispatchEvent(new CustomEvent('elt:theme', { detail: next }));
    });
  });
  // Reading progress hairline
  var bar = document.querySelector('.elt-report__progress');
  if (bar) {
    var onScroll = function () {
      var h = root.scrollHeight - window.innerHeight;
      bar.style.setProperty('--progress', (h > 0 ? Math.min(100, window.scrollY / h * 100) : 0).toFixed(1) + '%');
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
  }
  // Sticky nav / TOC active state
  var nav = document.querySelector('.elt-report__nav, .elt-toc');
  if (nav && 'IntersectionObserver' in window) {
    var links = Array.prototype.slice.call(nav.querySelectorAll('a[href^="#"]'));
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (en) {
        if (!en.isIntersecting) return;
        links.forEach(function (a) { a.classList.toggle('is-active', a.getAttribute('href') === '#' + en.target.id); });
      });
    }, { rootMargin: '-20% 0px -70% 0px' });
    links.forEach(function (a) { var s = document.querySelector(a.getAttribute('href')); if (s) io.observe(s); });
  }
  // Chart ⇄ table view: buttons carry data-elt-view="chart|table"; the figure gets data-view="table"
  document.addEventListener('click', function (e) {
    var b = e.target.closest('[data-elt-view]'); if (!b) return;
    var fig = b.closest('.elt-chart'); if (!fig) return;
    if (b.getAttribute('data-elt-view') === 'table') fig.setAttribute('data-view', 'table'); else fig.removeAttribute('data-view');
    fig.querySelectorAll('[data-elt-view]').forEach(function (x) { var on = x === b; x.classList.toggle('is-active', on); x.setAttribute('aria-pressed', on ? 'true' : 'false'); });
  });
  // Legend isolate: keys with .is-toggle and data-series dim the other series
  function isolate(k) {
    var fig = k.closest('.elt-chart'); if (!fig) return;
    k.classList.toggle('is-off');
    var off = Array.prototype.map.call(fig.querySelectorAll('.elt-legend__key.is-off'), function (x) { return x.getAttribute('data-series'); });
    fig.querySelectorAll('[data-series]').forEach(function (n) {
      if (n.classList.contains('elt-legend__key')) return;
      n.classList.toggle('elt-svg-dim', off.indexOf(n.getAttribute('data-series')) >= 0);
    });
  }
  document.addEventListener('click', function (e) { var k = e.target.closest('.elt-legend__key.is-toggle'); if (k) isolate(k); });
  document.addEventListener('keydown', function (e) { var k = e.target.closest && e.target.closest('.elt-legend__key.is-toggle'); if (k && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); isolate(k); } });
  // Print: open every collapsible so nothing is hidden on paper
  window.addEventListener('beforeprint', function () {
    document.querySelectorAll('details.elt-details:not([open])').forEach(function (d) { d.setAttribute('open', ''); d.setAttribute('data-elt-reclose', ''); });
  });
  window.addEventListener('afterprint', function () {
    document.querySelectorAll('details[data-elt-reclose]').forEach(function (d) { d.removeAttribute('open'); d.removeAttribute('data-elt-reclose'); });
  });
})();
</script>
```

### 12.4 Chart helper `eltChart` — before any chart script

Renders SVG lines and columns into a `.elt-chart` figure and adds the legend, table twin, crosshair, tooltip, keyboard access, and aria label. Options: `labels` (x categories), `series` (`[{ name, values, slot }]`, `slot` 1–4 or `'s-other'`, `'s-good'`, etc., `values` may contain `null` for gaps), `format` (value → string), `yMax` (a floor for the top of the scale; the helper rounds it up to clean ticks), `yMin`, `ticks` (preferred count, 3–8 chosen for clean steps; pass `exact: true` to force `yMax`/`ticks` verbatim), `endLabels` (lines), `area` (single-series lines), `stacked` (columns), `valueLabels` (`'max'` default for one series, `'all'`, `'none'`, or an array of indices), `target` + `targetLabel`, `annotations` (`[{ x, series, text }]`), `ariaLabel`.

```html
<script>
window.eltChart = (function () {
  var NS = 'http://www.w3.org/2000/svg', W = 860, H = 340;
  function el(tag, attrs, parent) { var n = document.createElementNS(NS, tag); for (var k in attrs) if (attrs[k] != null) n.setAttribute(k, attrs[k]); if (parent) parent.appendChild(n); return n; }
  function text(parent, x, y, str, cls, anchor) { var t = el('text', { x: x, y: y, 'class': cls, 'text-anchor': anchor || 'start' }, parent); t.textContent = str; return t; }
  function div(cls, parent) { var d = document.createElement('div'); d.className = cls; if (parent) parent.appendChild(d); return d; }
  function fmtDefault(v) { return v == null ? '–' : Number(v).toLocaleString(); }
  function slotClass(s) { return typeof s === 'number' ? 's' + s : (s || 's1'); }
  function swatchClass(s) { return 'is-' + slotClass(s).replace(/^s-?/, ''); }
  function niceStep(raw) { var p = Math.pow(10, Math.floor(Math.log(raw) / Math.LN10)), n = raw / p; return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 2.5 ? 2.5 : n <= 5 ? 5 : 10) * p; }
  function niceScale(max, preferred) {
    if (!(max > 0)) return { max: preferred, ticks: preferred };
    var best = null;
    for (var t = 3; t <= 8; t++) { var step = niceStep(max / t), top = step * t; if (top < max) continue; var cost = top / max + Math.abs(t - preferred) * 0.08; if (!best || cost < best.cost) best = { max: top, ticks: t, cost: cost }; }
    return best;
  }
  function barPath(x, y, w, h, r) { r = Math.max(0, Math.min(r, h / 2, w / 2)); var f = function (n) { return +n.toFixed(2); }; return 'M' + f(x) + ' ' + f(y + h) + ' V' + f(y + r) + ' Q' + f(x) + ' ' + f(y) + ' ' + f(x + r) + ' ' + f(y) + ' H' + f(x + w - r) + ' Q' + f(x + w) + ' ' + f(y) + ' ' + f(x + w) + ' ' + f(y + r) + ' V' + f(y + h) + ' Z'; }
  function titleOf(fig, fallback) { var t = fig.querySelector('.elt-chart__title'); return (t && t.textContent.trim()) || fallback; }

  function frame(fig) {
    var body = fig.querySelector('.elt-chart__body') || div('elt-chart__body', fig);
    body.textContent = '';
    var plot = div('elt-chart__plot is-auto', body), cross = div('elt-chart__crosshair', body), tip = div('elt-chart__tooltip', body);
    tip.setAttribute('role', 'status');
    return { body: body, plot: plot, cross: cross, tip: tip };
  }
  function legend(fig, series, kind) {
    var old = fig.querySelector('.elt-legend'); if (old) old.remove();
    if (series.length < 2) return;
    var lg = div('elt-legend');
    series.forEach(function (s, i) {
      var key = document.createElement('span'); key.className = 'elt-legend__key is-toggle'; key.setAttribute('data-series', String(i)); key.setAttribute('role', 'button'); key.setAttribute('aria-pressed', 'true'); key.tabIndex = 0;
      var sw = document.createElement('i'); sw.className = 'elt-legend__swatch ' + (kind === 'line' ? 'is-line ' : '') + swatchClass(s.slot);
      key.appendChild(sw); key.appendChild(document.createTextNode(s.name)); lg.appendChild(key);
    });
    var head = fig.querySelector('.elt-chart__head');
    if (head) head.insertAdjacentElement('afterend', lg); else fig.prepend(lg);
  }
  function tableTwin(fig, labels, series, format) {
    var old = fig.querySelector('.elt-chart__table'); if (old) old.remove();
    var wrap = div('elt-chart__table'), tw = div('elt-table-wrap is-flat', wrap);
    var t = document.createElement('table'); t.className = 'elt-table is-fit is-compact'; tw.appendChild(t);
    var hr = t.createTHead().insertRow(); hr.appendChild(document.createElement('th'));
    series.forEach(function (s) { var th = document.createElement('th'); th.className = 'is-num'; th.textContent = s.name; hr.appendChild(th); });
    var tb = t.createTBody();
    labels.forEach(function (lb, i) {
      var r = tb.insertRow(); r.insertCell().textContent = lb;
      series.forEach(function (s) { var c = r.insertCell(); c.className = 'is-num'; c.textContent = s.values[i] == null ? '–' : format(s.values[i]); });
    });
    var cap = fig.querySelector('.elt-chart__caption');
    if (cap) cap.insertAdjacentElement('beforebegin', wrap); else fig.appendChild(wrap);
  }
  function tooltip(f, title, rows, rect) {
    f.tip.textContent = '';
    div('elt-chart__tooltip-title', f.tip).textContent = title;
    rows.forEach(function (r) {
      var row = div('elt-chart__tooltip-row', f.tip);
      var k = document.createElement('i'); k.className = 'elt-chart__tooltip-key ' + slotClass(r.slot) + (rect ? ' is-rect' : ''); row.appendChild(k);
      var n = document.createElement('span'); n.className = 'elt-chart__tooltip-name'; n.textContent = r.name; row.appendChild(n);
      var v = document.createElement('b'); v.className = 'elt-chart__tooltip-val'; v.textContent = r.value; row.appendChild(v);
    });
  }
  function place(f, vx, vy) {
    var px = vx / W * 100, py = vy / H * 100;
    f.tip.style.left = px.toFixed(2) + '%'; f.tip.style.top = py.toFixed(2) + '%';
    f.tip.classList.toggle('is-flip', px > 68); f.tip.classList.toggle('is-start', px < 18); f.tip.classList.toggle('is-below', vy < 90);
    f.tip.classList.add('is-visible');
  }
  function hide(f) { f.cross.classList.remove('is-visible'); f.tip.classList.remove('is-visible'); }
  function scaleOf(series, opt) {
    var vals = [0]; series.forEach(function (s) { s.values.forEach(function (v) { if (v != null) vals.push(v); }); });
    var dataMax = Math.max.apply(null, vals), floor = opt.yMax != null ? Math.max(opt.yMax, dataMax) : dataMax;
    var ns = niceScale(floor, opt.ticks || 5), min = opt.yMin != null ? opt.yMin : Math.min(0, Math.min.apply(null, vals));
    var max = opt.exact && opt.yMax != null ? opt.yMax : ns.max, ticks = opt.exact && opt.ticks ? opt.ticks : ns.ticks;
    if (max <= min) max = min + ticks;
    return { min: min, max: max, ticks: ticks };
  }
  function axes(svg, L, R, T, B, sc, labels, format, band) {
    var pw = W - L - R, ph = H - T - B, g = el('g', null, svg);
    var y = function (v) { return T + ph - (v - sc.min) / (sc.max - sc.min) * ph; };
    for (var i = 0; i <= sc.ticks; i++) {
      var v = sc.min + (sc.max - sc.min) * i / sc.ticks, yy = y(v);
      el('line', { x1: L, x2: W - R, y1: yy, y2: yy, 'class': v === 0 ? 'elt-svg-axis' : 'elt-svg-grid' }, g);
      text(g, L - 8, yy + 4, format(v), 'elt-svg-tick', 'end');
    }
    var step = Math.max(1, Math.ceil(labels.length / Math.max(1, Math.floor(pw / 64))));
    var x = function (i) { return band ? L + (i + 0.5) * pw / labels.length : L + (labels.length > 1 ? i / (labels.length - 1) * pw : pw / 2); };
    labels.forEach(function (lb, i) { if (i % step && i !== labels.length - 1) return; text(g, x(i), H - B + 20, lb, 'elt-svg-tick', 'middle'); });
    return { pw: pw, ph: ph, x: x, y: y };
  }
  function keyboard(hit, n, show) {
    var i = 0;
    hit.addEventListener('focus', function () { show(i); });
    hit.addEventListener('keydown', function (e) {
      if (e.key === 'ArrowRight') i = Math.min(n - 1, i + 1); else if (e.key === 'ArrowLeft') i = Math.max(0, i - 1); else if (e.key === 'Home') i = 0; else if (e.key === 'End') i = n - 1; else return;
      e.preventDefault(); show(i);
    });
  }

  function line(fig, opt) {
    var labels = opt.labels, series = opt.series, format = opt.format || fmtDefault;
    var f = frame(fig), L = 56, R = opt.endLabels ? 96 : 20, T = 20, B = 40;
    var svg = el('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', 'aria-label': opt.ariaLabel || titleOf(fig, 'Line chart') }, f.plot);
    var sc = scaleOf(series, opt), a = axes(svg, L, R, T, B, sc, labels, format, false);
    if (opt.target != null) { el('line', { x1: L, x2: W - R, y1: a.y(opt.target), y2: a.y(opt.target), 'class': 'elt-svg-ref' }, svg); text(svg, W - R, a.y(opt.target) - 6, opt.targetLabel || 'Target', 'elt-svg-annot', 'end'); }
    series.forEach(function (s, si) {
      var g = el('g', { 'class': slotClass(s.slot), 'data-series': String(si) }, svg), d = '', on = false, first = -1, last = -1;
      s.values.forEach(function (v, i) { if (v == null) { on = false; return; } if (first < 0) first = i; d += (on ? 'L' : 'M') + a.x(i).toFixed(1) + ' ' + a.y(v).toFixed(1) + ' '; on = true; last = i; });
      if (last < 0) return;
      if (opt.area && series.length === 1) el('path', { d: d + 'L' + a.x(last).toFixed(1) + ' ' + a.y(sc.min).toFixed(1) + ' L' + a.x(first).toFixed(1) + ' ' + a.y(sc.min).toFixed(1) + ' Z', 'class': 'elt-svg-area' }, g);
      el('path', { d: d, 'class': 'elt-svg-line' }, g);
      s.values.forEach(function (v, i) { if (v != null) el('circle', { cx: a.x(i).toFixed(1), cy: a.y(v).toFixed(1), r: 4, 'class': 'elt-svg-dot' }, g); });
      if (opt.endLabels) text(g, a.x(last) + 10, a.y(s.values[last]) + 4, format(s.values[last]), 'elt-svg-value');
    });
    (opt.annotations || []).forEach(function (an) { var s = series[an.series || 0], v = s && s.values[an.x]; if (v == null) return; text(svg, a.x(an.x), a.y(v) - 12, an.text, 'elt-svg-annot', 'middle'); });
    var hit = el('rect', { x: L, y: T, width: a.pw, height: a.ph, 'class': 'elt-svg-hit', tabindex: 0, 'aria-label': 'Hover or use arrow keys to read values' }, svg);
    function show(i) {
      var vx = a.x(i), top = Infinity;
      series.forEach(function (s) { if (s.values[i] != null) top = Math.min(top, a.y(s.values[i])); });
      f.cross.style.left = (vx / W * 100).toFixed(2) + '%'; f.cross.classList.add('is-visible');
      tooltip(f, labels[i], series.map(function (s) { return { slot: s.slot, name: s.name, value: s.values[i] == null ? '–' : format(s.values[i]) }; }), false);
      place(f, vx, isFinite(top) ? top : T);
    }
    hit.addEventListener('pointermove', function (e) { var r = f.plot.getBoundingClientRect(); var vx = (e.clientX - r.left) / r.width * W; show(Math.max(0, Math.min(labels.length - 1, Math.round((vx - L) / a.pw * (labels.length - 1))))); });
    hit.addEventListener('pointerleave', function () { hide(f); }); hit.addEventListener('blur', function () { hide(f); });
    keyboard(hit, labels.length, show);
    legend(fig, series, 'line'); tableTwin(fig, labels, series, format);
  }

  function columns(fig, opt) {
    var labels = opt.labels, series = opt.series, format = opt.format || fmtDefault, stacked = !!opt.stacked;
    var f = frame(fig), L = 56, R = 20, T = 24, B = 40;
    var svg = el('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', 'aria-label': opt.ariaLabel || titleOf(fig, 'Column chart') }, f.plot);
    var totals = labels.map(function (_, i) { return series.reduce(function (acc, s) { return acc + (s.values[i] || 0); }, 0); });
    var sc = scaleOf(stacked ? [{ values: totals }] : series, opt), a = axes(svg, L, R, T, B, sc, labels, format, true);
    var band = a.pw / labels.length, gap = 2, n = stacked ? 1 : series.length, bw = Math.min(24, Math.max(4, (band * 0.72 - gap * (n - 1)) / n));
    var groups = series.map(function (s, si) { return el('g', { 'class': slotClass(s.slot), 'data-series': String(si) }, svg); });
    var lab = opt.valueLabels != null ? opt.valueLabels : (series.length === 1 && !stacked ? 'max' : 'none');
    var maxI = totals.indexOf(Math.max.apply(null, totals));
    labels.forEach(function (lb, i) {
      var x0 = L + i * band + (band - (bw * n + gap * (n - 1))) / 2, cum = 0;
      series.forEach(function (s, si) {
        var v = s.values[i]; if (v == null) return;
        var x = stacked ? x0 : x0 + si * (bw + gap), y1 = a.y(cum), y2 = a.y(cum + v), top = stacked && si === series.length - 1 || !stacked;
        var h = Math.max(0, y1 - y2 - (stacked && si > 0 ? gap : 0));
        el('path', { d: barPath(x, y2, bw, h, top ? 4 : 0), 'class': 'elt-svg-bar', 'data-i': String(i) }, groups[si]);
        if (stacked) cum += v;
      });
      var want = lab === 'all' || (lab === 'max' && i === maxI) || (Array.isArray(lab) && lab.indexOf(i) >= 0);
      if (want) text(svg, x0 + (bw * n + gap * (n - 1)) / 2, a.y(stacked ? totals[i] : Math.max.apply(null, series.map(function (s) { return s.values[i] || 0; }))) - 6, format(stacked ? totals[i] : series.length === 1 ? series[0].values[i] : Math.max.apply(null, series.map(function (s) { return s.values[i] || 0; }))), 'elt-svg-value', 'middle');
    });
    if (opt.target != null) { el('line', { x1: L, x2: W - R, y1: a.y(opt.target), y2: a.y(opt.target), 'class': 'elt-svg-ref' }, svg); text(svg, W - R, a.y(opt.target) - 6, opt.targetLabel || 'Target', 'elt-svg-annot', 'end'); }
    var hit = el('rect', { x: L, y: T, width: a.pw, height: a.ph, 'class': 'elt-svg-hit is-mark', tabindex: 0, 'aria-label': 'Hover or use arrow keys to read values' }, svg);
    function show(i) {
      svg.querySelectorAll('.elt-svg-bar').forEach(function (b) { b.classList.toggle('is-hover', b.getAttribute('data-i') === String(i)); });
      tooltip(f, labels[i], series.map(function (s) { return { slot: s.slot, name: s.name, value: s.values[i] == null ? '–' : format(s.values[i]) }; }), true);
      place(f, a.x(i), a.y(stacked ? totals[i] : Math.max.apply(null, series.map(function (s) { return s.values[i] || 0; }))));
    }
    hit.addEventListener('pointermove', function (e) { var r = f.plot.getBoundingClientRect(); var vx = (e.clientX - r.left) / r.width * W; show(Math.max(0, Math.min(labels.length - 1, Math.floor((vx - L) / band)))); });
    hit.addEventListener('pointerleave', function () { hide(f); svg.querySelectorAll('.elt-svg-bar.is-hover').forEach(function (b) { b.classList.remove('is-hover'); }); });
    hit.addEventListener('blur', function () { hide(f); });
    keyboard(hit, labels.length, show);
    legend(fig, series, 'bar'); tableTwin(fig, labels, series, format);
  }

  function spark(elm, values, opt) {
    opt = opt || {}; var w = elm.clientWidth || 120, h = elm.clientHeight || 32, n = values.length;
    var min = Math.min.apply(null, values), max = Math.max.apply(null, values), span = (max - min) || 1;
    var x = function (i) { return n > 1 ? 2 + i / (n - 1) * (w - 4) : w / 2; }, y = function (v) { return h - 3 - (v - min) / span * (h - 6); };
    elm.textContent = ''; elm.classList.add('elt-sparkline');
    var svg = el('svg', { viewBox: '0 0 ' + w + ' ' + h, 'class': slotClass(opt.slot), 'aria-hidden': 'true' }, elm);
    var d = values.map(function (v, i) { return (i ? 'L' : 'M') + x(i).toFixed(1) + ' ' + y(v).toFixed(1); }).join(' ');
    if (opt.area !== false) el('path', { d: d + ' L' + x(n - 1).toFixed(1) + ' ' + h + ' L' + x(0).toFixed(1) + ' ' + h + ' Z', 'class': 'is-area' }, svg);
    el('path', { d: d, 'class': 'is-line' }, svg);
    el('circle', { cx: x(n - 1).toFixed(1), cy: y(values[n - 1]).toFixed(1), r: 3 }, svg);
  }

  return { line: line, columns: columns, spark: spark, tableTwin: tableTwin };
})();
</script>
```

### 12.5 Sortable tables — for any `.elt-table` with `th.is-sortable`

```html
<script>
document.addEventListener('click', function (e) {
  var th = e.target.closest('th.is-sortable'); if (!th) return;
  var table = th.closest('table'), tbody = table.tBodies[0], idx = Array.prototype.indexOf.call(th.parentNode.children, th);
  var dir = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';
  Array.prototype.forEach.call(th.parentNode.children, function (h) { h.removeAttribute('aria-sort'); });
  th.setAttribute('aria-sort', dir);
  var num = function (s) { var n = parseFloat(String(s).replace(/[^0-9.\-]/g, '')); return isNaN(n) ? null : n; };
  var rows = Array.prototype.slice.call(tbody.rows).filter(function (r) { return !r.classList.contains('is-total'); });
  rows.sort(function (ra, rb) {
    var A = ra.cells[idx].textContent.trim(), B = rb.cells[idx].textContent.trim(), na = num(A), nb = num(B);
    var c = (na != null && nb != null) ? na - nb : A.localeCompare(B, undefined, { numeric: true });
    return dir === 'ascending' ? c : -c;
  });
  rows.forEach(function (r) { tbody.appendChild(r); });
  var total = tbody.querySelector('tr.is-total'); if (total) tbody.appendChild(total);
});
</script>
```

### 12.6 Print

Handled by §12.3 (`beforeprint` opens every `.elt-details`) and by the `@media print` block in design.css. When the report needs an explicit print control, use the toolbar button rather than adding a second “Download PDF” control.

### 12.7 Texture pattern — only when identity must survive without colour

Add once, right after `<body>`, then put `.elt-svg-hatch` on slot-2 marks (and `.elt-svg-hatch.is-135` on slot-4 marks) inside a `@media print` or `forced-colors` context, or behind an accessibility toggle. Never on by default.

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
  <pattern id="elt-hatch" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)"><rect width="6" height="6" fill="var(--chart-2)"/><line x1="0" y1="0" x2="0" y2="6" stroke="var(--chart-surface)" stroke-width="2"/></pattern>
  <pattern id="elt-hatch-135" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(135)"><rect width="6" height="6" fill="var(--chart-4)"/><line x1="0" y1="0" x2="0" y2="6" stroke="var(--chart-surface)" stroke-width="2"/></pattern>
</defs></svg>
```

---

## 13. Appendix

### 13.1 Provenance and maintenance

Foundation tokens and component mechanics mirror `eltglobal-os`; chart roles are derived from those foundations and have been checked for contrast and color-vision separation. The detailed source paths are recorded in the header of design.css, which is the mechanical source of truth.

When a token or component changes, update design.css first, then update only the judgment or public contract affected here. Recheck contrast, both themes, forced colors, print, responsive reflow, and the canonical examples. Do not preserve a duplicated value table in this file.

### 13.2 Brand assets

Wordmark (122 × 22), fixed colours, `.elt-logo`:

```html
<span class="elt-logo" role="img" aria-label="ELT Global"><svg width="122" height="22" viewBox="0 0 122 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.8145 12.9957H59.8116V17.8843C58.4473 18.5951 56.8842 18.9359 54.9513 18.9359C50.0911 18.9359 46.851 15.8664 46.851 11.1198C46.851 6.37326 50.0911 3.30377 54.9513 3.30377C57.8287 3.30377 60.0255 4.16457 61.8069 5.84248V3.38231C60.0446 2.12342 57.7657 1.39947 54.8661 1.39947C48.7553 1.39947 44.7192 5.23642 44.7192 11.1198C44.7192 17.0033 48.7553 20.8402 54.8661 20.8402C57.8504 20.8402 60.039 20.215 61.8296 18.879V11.0914H53.8145V12.9957Z" fill="#0868D8"/>
<path d="M70.0576 13.9336C70.0576 17.0033 72.1609 19.1349 75.1168 19.1349C78.1011 19.1349 80.2045 17.0033 80.2045 13.9336C80.2045 10.8639 78.1011 8.70381 75.1168 8.70381C72.1609 8.70381 70.0576 10.8639 70.0576 13.9336ZM82.1374 13.9336C82.1374 17.9695 79.2098 20.8402 75.1168 20.8402C71.0241 20.8402 68.125 17.9695 68.125 13.9336C68.125 9.86917 71.0241 6.99846 75.1168 6.99846C79.2098 6.99846 82.1374 9.86917 82.1374 13.9336Z" fill="#0868D8"/>
<path d="M64.1914 20.5561H66.0957V1.60948H64.1914V20.5561Z" fill="#0868D8"/>
<path d="M115.454 20.5561H117.358V1.62681H115.454V20.5561Z" fill="#0868D8"/>
<path d="M90.8684 19.1349C89.1042 19.1349 87.4077 18.3188 86.4915 16.7783C85.4621 15.0484 85.5085 12.5575 86.6193 10.8762C87.5594 9.45249 89.1849 8.70399 90.8684 8.70399C93.8245 8.70399 95.9278 10.8637 95.9278 13.934C95.9278 17.0034 93.8245 19.1349 90.8684 19.1349ZM91.1239 6.99882C88.7935 6.99882 86.9459 7.90836 85.8948 9.38642L85.8374 9.47289V1.66143H83.9331V20.5564H85.7888V18.3123C86.8313 19.8769 88.6962 20.8402 91.1239 20.8402C95.0464 20.8402 97.8603 17.9697 97.8603 13.934C97.8603 9.89752 95.0464 6.99882 91.1239 6.99882Z" fill="#0868D8"/>
<path d="M106.568 19.1349C103.612 19.1349 101.509 17.0034 101.509 13.934C101.509 10.8637 103.612 8.70399 106.568 8.70399C109.552 8.70399 111.656 10.8637 111.656 13.934C111.656 17.0034 109.552 19.1349 106.568 19.1349ZM111.66 7.23532V9.54836C110.594 7.97155 108.723 6.99882 106.283 6.99882C102.39 6.99882 99.5762 9.89751 99.5762 13.934C99.5762 17.9697 102.39 20.8402 106.283 20.8402C108.722 20.8402 110.62 19.868 111.66 18.2927V20.5564H113.489V7.23532H111.66Z" fill="#0868D8"/>
<path d="M119.365 1.98536V2.61453H119.687C119.807 2.61453 119.896 2.58691 119.955 2.53166C120.013 2.47642 120.042 2.39933 120.042 2.30003C120.042 2.09025 119.924 1.98536 119.687 1.98536H119.365ZM119.365 2.78748V3.47443H119.146V1.81006H119.687C119.874 1.81006 120.017 1.85573 120.116 1.9469C120.214 2.03826 120.263 2.14983 120.263 2.28198C120.263 2.41413 120.227 2.52408 120.155 2.61218C120.083 2.70028 119.975 2.75553 119.831 2.77792L120.29 3.47443H120.016L119.579 2.78748H119.365Z" fill="#0868D8"/>
<path d="M119.718 1.28713C118.971 1.28713 118.363 1.89482 118.363 2.64188C118.363 3.38894 118.971 3.99663 119.718 3.99663C120.465 3.99663 121.072 3.38894 121.072 2.64188C121.072 1.89482 120.465 1.28713 119.718 1.28713ZM119.718 4.12427C118.9 4.12427 118.235 3.45917 118.235 2.64188C118.235 1.82441 118.9 1.15949 119.718 1.15949C120.535 1.15949 121.2 1.82441 121.2 2.64188C121.2 3.45917 120.535 4.12427 119.718 4.12427Z" fill="#0868D8"/>
<path d="M3.78044 12.9155H10.5022V9.13322H3.78044V5.284H13.2319C12.5244 4.57647 11.9235 3.97565 11.3227 3.37284C10.7313 2.78158 10.142 2.19411 9.45146 1.50175H0V16.7607C0.721064 17.4818 1.30275 18.0635 1.88264 18.6452C2.4701 19.2308 3.05558 19.8163 3.78622 20.5469H13.2301V16.7665H3.78044V12.9155Z" fill="#0868D8"/>
<path d="M26.8525 18.6298C26.2688 18.0461 25.6852 17.4624 24.989 16.7663H19.3102V1.48021H15.5298V20.5467H28.7599V20.5351C28.0446 19.82 27.4476 19.2248 26.8525 18.6298Z" fill="#0868D8"/>
<path d="M28.7402 1.47154V5.24656H33.9541V20.5467H37.7349V1.47154H28.7402Z" fill="#0868D8"/>
<path d="M42.0087 1.46682H37.7891C39.433 2.94325 40.3549 3.77101 41.9988 5.24744H42.0087V1.46682Z" fill="#ED1C24"/>
</svg></span>
```

Mark, blue on light (`.elt-brandmark`):

```html
<span class="elt-brandmark" role="img" aria-label="ELT Global"><svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 50C0 22.3858 22.3858 0 50 0V0C77.6142 0 100 22.3858 100 50V50C100 77.6142 77.6142 100 50 100V100C22.3858 100 0 77.6142 0 50V50Z" fill="#0868D8"/><path d="M42.2854 55.2371H60.5733V44.9467H42.2854V34.4742H68C66.075 32.5492 64.4404 30.9145 62.8057 29.2745C61.1966 27.6658 59.5934 26.0675 57.7146 24.1838H32V65.6988C33.9618 67.6606 35.5444 69.2432 37.1221 70.8258C38.7204 72.4193 40.3133 74.0122 42.3012 76H67.9951V65.7146H42.2854V55.2371Z" fill="white"/></svg></span>
```

Mark, inverted for dark surfaces: the same paths with the circle `fill="white"` and the "E" `fill="#0868D8"`.

### 13.3 Validation contract

The published chart and semantic color tokens are the approved set. Do not add, replace, reorder, or repurpose a data color without rerunning contrast and color-vision checks on primary and tinted surfaces in both themes. Text must meet WCAG AA for its role; material marks below the intended contrast threshold require direct labels, a non-color cue, and an accessible data alternative.

The operational conclusion is authoritative; raw validator commands and historical candidate colors are intentionally omitted because they do not guide page composition and quickly become stale.

