Blocks

A dashboard is a list of sections; a section is a list of blocks. These are all 37 of them, rendered live from the JSON shown beside each one. The machine-readable contract is at /api/schema. The older names `text` and `image` are still accepted and parse as `prose` and `figure`.

Layout

A section is a grid; a block is a rectangle on it. These eleven fields are the whole layout system — there is no width class and no wide variant of any block.

on a section

columnsTrack width for this section. Default 12, max 24. Default spans scale with it.
gapnone · xs · sm · md (default) · lg · xl — the space between cells.
rowHeightRow height in px. Without it every row sizes to its tallest cell and `rowSpan` does nothing.
alignstart · center · end · stretch (default) — how blocks fill the height of their row.
denseLet later blocks backfill earlier gaps. Off by default: it reorders the visual sequence away from the document order.
hiddenIn the document, not on the page.
only'screen' or 'print'.

on a block

spanColumns wide. Clamped to the section track.
rowSpanRows tall. Needs the section to set `rowHeight`.
colStartThe column this block starts in, 1-based. This is what leaves a deliberate gap, or lines a block up with one in another section.
rowStartThe row this block starts in, 1-based. Rarely needed.
alignOverrides the section's alignment for one block.
minHeightExplicit pixels, when a row count is the wrong unit.
hidden / onlySame two flags, per block.

Below 768px the track collapses to two columns and every span is quantised to half or full, so a strip of KPI tiles stays a strip on a phone. Explicit starts are dropped there — a gap designed for twelve columns is not a gap in two. Inside the print document the section’s own track always applies, whatever the window is doing.

What survives an html block

Markup is sanitised when the spec is parsed, not when it is drawn, so what is stored is already safe and everything outside these lists is gone before it reaches the page. Removal is silent: a POST that failed on a formatting detail would be worse than one that renders slightly plainer than intended.

Tags
pbrhrspandivstrongbemiusdelinssmallsubsupmarkabbrciteqblockquotetimeaddresscodeprekbdsampvaraulollidldtddtablecaptioncolgroupcoltheadtbodytfoottrthtdh2h3h4h5h6sectionarticleheaderfooterfigurefigcaption
Attributes
hreftitleclasslangdircolspanrowspanscopeheadersspandatetimecitestartreversedvalue
Classes
hb-gridhb-grid-2hb-grid-3hb-grid-4hb-rowhb-stackhb-cardhb-panelhb-dividerhb-badgehb-keyhb-valuehb-mutedhb-stronghb-monohb-righthb-centerhb-filleyebrowmeasuremeasure-widenumhangingtone-neutraltone-positivetone-warningtone-dangertone-infotone-accent

Everything else is dropped, including script, every on* handler, inline style, id, img, iframe, and any href that is not http, https or mailto. A class outside the list is removed while the element around it stays, so a fragment degrades to plain rather than disappearing. For a third-party PAGE use embed, which is a sandboxed iframe.

stat

One KPI. `rail` draws a progress bar, `delta` shows movement, `spark` the history.

Open commitments
4+2
2 past 7 days
{
  "type": "stat",
  "label": "Open commitments",
  "value": 4,
  "sublabel": "2 past 7 days",
  "tone": "warning",
  "rail": 0.4,
  "delta": {
    "value": "+2",
    "direction": "up",
    "tone": "danger"
  },
  "spark": [
    1,
    1,
    2,
    2,
    3,
    2,
    4
  ]
}

stats

A row of KPIs — the strip most reports open with.

Customers monitored
Monitored
12
Red
1+1
Yellow
3
Green
8
{
  "type": "stats",
  "title": "Customers monitored",
  "items": [
    {
      "label": "Monitored",
      "value": 12
    },
    {
      "label": "Red",
      "value": 1,
      "tone": "danger",
      "delta": {
        "value": "+1",
        "direction": "up",
        "tone": "danger"
      }
    },
    {
      "label": "Yellow",
      "value": 3,
      "tone": "warning"
    },
    {
      "label": "Green",
      "value": 8,
      "tone": "positive",
      "spark": [
        6,
        7,
        7,
        8,
        8
      ]
    }
  ]
}

bignumber

One number, sized to be read across a room.

261,009.25
Value at risk

Carried by missing and differing invoices — 84.9% of the range total.

{
  "type": "bignumber",
  "value": "261,009.25",
  "label": "Value at risk",
  "context": "Carried by missing and differing invoices — 84.9% of the range total.",
  "tone": "danger"
}

gauge

A value against NAMED thresholds. Without `bands` this is a `stat` with a rail and more pixels — use the stat.

Response SLA
72%0100
Threads replied to inside 24 hours
≤ 60%≤ 85%≤ 100%
{
  "type": "gauge",
  "title": "Response SLA",
  "value": 72,
  "min": 0,
  "max": 100,
  "unit": "%",
  "label": "Threads replied to inside 24 hours",
  "tone": "warning",
  "bands": [
    {
      "upTo": 60,
      "tone": "danger"
    },
    {
      "upTo": 85,
      "tone": "warning"
    },
    {
      "upTo": 100,
      "tone": "positive"
    }
  ]
}

segments

Parts of a whole in one bar, with a legend.

Value at risk
307,499.25Total invoice value in range
Missing in eSpeed79393.75
Differing181615.5
Incomplete19980
Matching26510
{
  "type": "segments",
  "title": "Value at risk",
  "total": "307,499.25",
  "totalLabel": "Total invoice value in range",
  "segments": [
    {
      "label": "Missing in eSpeed",
      "value": 79393.75,
      "tone": "danger"
    },
    {
      "label": "Differing",
      "value": 181615.5,
      "tone": "warning"
    },
    {
      "label": "Incomplete",
      "value": 19980,
      "tone": "info"
    },
    {
      "label": "Matching",
      "value": 26510,
      "tone": "positive"
    }
  ]
}

progressList

Labelled bars. Scales to the largest value unless you set `max`.

Discrepancies by field
payment.date5
payment.amount1
vendor.name3
{
  "type": "progressList",
  "title": "Discrepancies by field",
  "items": [
    {
      "label": "payment.date",
      "value": 5,
      "tone": "warning"
    },
    {
      "label": "payment.amount",
      "value": 1,
      "tone": "danger"
    },
    {
      "label": "vendor.name",
      "value": 3
    }
  ]
}

scorecard

Weighted criteria and the grade they add up to. Says why, not just what.

Account health — Ripple Fiber
C+
Watch

Delivery is holding; the drag is entirely on responsiveness and billing hygiene.

  • Build delivery vs plan×388/100
  • Response time to escalations×254/100

    Median 31h against a 24h commitment.

  • Invoice accuracy×262/100
  • Commitment closure×171/100
{
  "type": "scorecard",
  "title": "Account health — Ripple Fiber",
  "scale": 100,
  "overall": {
    "grade": "C+",
    "label": "Watch",
    "detail": "Delivery is holding; the drag is entirely on responsiveness and billing hygiene.",
    "tone": "warning"
  },
  "criteria": [
    {
      "label": "Build delivery vs plan",
      "score": 88,
      "weight": 3,
      "tone": "positive"
    },
    {
      "label": "Response time to escalations",
      "score": 54,
      "weight": 2,
      "tone": "danger",
      "note": "Median 31h against a 24h commitment."
    },
    {
      "label": "Invoice accuracy",
      "score": 62,
      "weight": 2,
      "tone": "warning"
    },
    {
      "label": "Commitment closure",
      "score": 71,
      "weight": 1
    }
  ]
}

Data and records

Charts, matrices, tables, people.

chart

bar · barStacked · barHorizontal · line · area · pie · donut · scatter · radar · funnel — or a raw ECharts `option`.

Escalation keywords by week
{
  "type": "chart",
  "title": "Escalation keywords by week",
  "kind": "area",
  "categories": [
    "W28",
    "W29",
    "W30",
    "W31",
    "W32"
  ],
  "series": [
    {
      "name": "delay",
      "data": [
        1,
        2,
        2,
        4,
        6
      ],
      "tone": "danger"
    },
    {
      "name": "invoice",
      "data": [
        0,
        1,
        3,
        2,
        3
      ],
      "tone": "warning"
    }
  ],
  "unit": "",
  "height": 240
}

heatmap

A numeric matrix. `values` is row-major: `values[y][x]`.

Contact volume
{
  "type": "heatmap",
  "title": "Contact volume",
  "xLabels": [
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri"
  ],
  "yLabels": [
    "Email",
    "Call",
    "Meeting"
  ],
  "values": [
    [
      12,
      8,
      15,
      6,
      9
    ],
    [
      2,
      4,
      1,
      3,
      5
    ],
    [
      0,
      1,
      2,
      0,
      1
    ]
  ],
  "height": 220
}

statusMatrix

Entities down, checks across, a status in every cell. Categorical, where heatmap is numeric.

Build readiness by route
RowGISWAYPRMSPLABT
Route 14 — Bridgend spine4.2 km, 288f✓✓✓!—
Route 17 — Llanelli lateral✓!✗——
Route 21 — Docks feeder✓✓✓✓✓
CompleteIn progressBlocked
{
  "type": "statusMatrix",
  "title": "Build readiness by route",
  "columns": [
    {
      "label": "GIS survey",
      "short": "GIS"
    },
    {
      "label": "Wayleave",
      "short": "WAY"
    },
    {
      "label": "Permit",
      "short": "PRM"
    },
    {
      "label": "Splice plan",
      "short": "SPL"
    },
    {
      "label": "As-built",
      "short": "ABT"
    }
  ],
  "rows": [
    {
      "label": "Route 14 — Bridgend spine",
      "note": "4.2 km, 288f",
      "cells": [
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "warning",
          "label": "!",
          "note": "Awaiting closure schedule from Uros"
        },
        null
      ]
    },
    {
      "label": "Route 17 — Llanelli lateral",
      "cells": [
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "warning",
          "label": "!"
        },
        {
          "tone": "danger",
          "label": "✗",
          "note": "Highways rejected 06 Aug — resubmission not filed"
        },
        null,
        null
      ]
    },
    {
      "label": "Route 21 — Docks feeder",
      "cells": [
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "positive",
          "label": "✓"
        },
        {
          "tone": "positive",
          "label": "✓"
        }
      ]
    }
  ],
  "legend": [
    {
      "label": "Complete",
      "tone": "positive"
    },
    {
      "label": "In progress",
      "tone": "warning"
    },
    {
      "label": "Blocked",
      "tone": "danger"
    }
  ]
}

comparison

Two states of the same rows, with the movement between them written down.

Quoted against as-built

Route 14 — Bridgend spine, closed 11 Aug.

MeasureQuotedBid pack, 02 JunAs-builtSurvey, 11 AugΔ
Duct (m)Diversion around the culvert at ch. 2100.42004470+270
Chambers18180
Splice closures911+2
Days on site2219−3
{
  "type": "comparison",
  "title": "Quoted against as-built",
  "caption": "Route 14 — Bridgend spine, closed 11 Aug.",
  "numeric": true,
  "left": {
    "label": "Quoted",
    "caption": "Bid pack, 02 Jun"
  },
  "right": {
    "label": "As-built",
    "caption": "Survey, 11 Aug"
  },
  "rows": [
    {
      "label": "Duct (m)",
      "left": 4200,
      "right": 4470,
      "delta": "+270",
      "tone": "warning",
      "note": "Diversion around the culvert at ch. 2100."
    },
    {
      "label": "Chambers",
      "left": 18,
      "right": 18,
      "delta": "0",
      "tone": "positive"
    },
    {
      "label": "Splice closures",
      "left": 9,
      "right": 11,
      "delta": "+2",
      "tone": "warning"
    },
    {
      "label": "Days on site",
      "left": 22,
      "right": 19,
      "delta": "−3",
      "tone": "positive"
    }
  ]
}

table

Cells can be plain, or carry a tone, a pill, a sparkline, a link or an inline bar.

Open commitments
3 rows
CommitmentOwnerFlagged / dueStatus / age
Site walk confirmation to Ripple opsLindon2026-08-0410 days open
Invoice dispute resolution dateUnassigned2026-07-2916 days open
Fibre splice schedule sharedUros2026-08-08CLOSED — sent 11 Aug
{
  "type": "table",
  "title": "Open commitments",
  "searchable": true,
  "columns": [
    {
      "key": "commitment",
      "label": "Commitment"
    },
    {
      "key": "owner",
      "label": "Owner"
    },
    {
      "key": "due",
      "label": "Flagged / due"
    },
    {
      "key": "status",
      "label": "Status / age"
    }
  ],
  "rows": [
    {
      "commitment": "Site walk confirmation to Ripple ops",
      "owner": "Lindon",
      "due": "2026-08-04",
      "status": {
        "value": "10 days open",
        "tone": "warning",
        "badge": true
      }
    },
    {
      "commitment": "Invoice dispute resolution date",
      "owner": "Unassigned",
      "due": "2026-07-29",
      "status": {
        "value": "16 days open",
        "tone": "danger",
        "badge": true
      }
    },
    {
      "commitment": "Fibre splice schedule shared",
      "owner": "Uros",
      "due": "2026-08-08",
      "status": {
        "value": "CLOSED — sent 11 Aug",
        "tone": "positive",
        "badge": true
      }
    }
  ]
}

list

A flat list, optionally numbered, each item with its own tone.

Recommended actions
  1. 1
    Confirm the site walk is scheduled

    Lindon to confirm with the Ripple account owner this week.

    This week
  2. 2
    Give the invoice dispute a resolution date

    Unowned for 16 days — the longest-running open item.

    Today
{
  "type": "list",
  "title": "Recommended actions",
  "ordered": true,
  "items": [
    {
      "title": "Confirm the site walk is scheduled",
      "detail": "Lindon to confirm with the Ripple account owner this week.",
      "meta": "This week",
      "tone": "warning"
    },
    {
      "title": "Give the invoice dispute a resolution date",
      "detail": "Unowned for 16 days — the longest-running open item.",
      "meta": "Today",
      "tone": "danger"
    }
  ]
}

timeline

Dated events on a rail. `when` is free text, not a date type.

Account timeline
  1. 29 Jul
    Weekly call

    Tone neutral. Invoice dispute raised.

  2. 04 Aug
    Site walk committed

    No confirmation since.

  3. 11 Aug
    Splice schedule sent
{
  "type": "timeline",
  "title": "Account timeline",
  "events": [
    {
      "when": "29 Jul",
      "title": "Weekly call",
      "detail": "Tone neutral. Invoice dispute raised.",
      "tone": "neutral"
    },
    {
      "when": "04 Aug",
      "title": "Site walk committed",
      "detail": "No confirmation since.",
      "tone": "warning"
    },
    {
      "when": "11 Aug",
      "title": "Splice schedule sent",
      "tone": "positive"
    }
  ]
}

fields

Labelled values — the key/value and definition-list block. `inline` gives the spec-sheet form.

Trend analysis
Direction
Declining — second consecutive week of slower replies.
Most repeated concern
"delay" appeared 6 times, up from 1 last week.
Improving
Splice scheduling is now shared proactively.
{
  "type": "fields",
  "title": "Trend analysis",
  "fields": [
    {
      "label": "Direction",
      "value": "Declining — second consecutive week of slower replies.",
      "tone": "warning"
    },
    {
      "label": "Most repeated concern",
      "value": "\"delay\" appeared 6 times, up from 1 last week."
    },
    {
      "label": "Improving",
      "value": "Splice scheduling is now shared proactively.",
      "tone": "positive"
    }
  ]
}

pills

A row of tags — statuses, applied filters, counts. `solid` makes it a status strip.

Sources
SharePoint emails287Call transcripts23eSpeednot connected
{
  "type": "pills",
  "title": "Sources",
  "pills": [
    {
      "label": "SharePoint emails",
      "value": 287,
      "tone": "info"
    },
    {
      "label": "Call transcripts",
      "value": 23,
      "tone": "info"
    },
    {
      "label": "eSpeed",
      "value": "not connected",
      "tone": "neutral"
    }
  ]
}

people

Named people — owners, contacts, the escalation path.

Escalation path
  • Lindon Marshprimary
    Account lead, Essentia
  • Uros Petrovic
    Build manager

    Owns the splice schedule and closure bookings.

  • Dana Whitfieldescalation
    Ripple Fiber — ops director
  • Unassigned16 days
    Billing dispute owner
{
  "type": "people",
  "title": "Escalation path",
  "columns": 2,
  "people": [
    {
      "name": "Lindon Marsh",
      "role": "Account lead, Essentia",
      "status": "primary",
      "tone": "positive"
    },
    {
      "name": "Uros Petrovic",
      "role": "Build manager",
      "detail": "Owns the splice schedule and closure bookings."
    },
    {
      "name": "Dana Whitfield",
      "role": "Ripple Fiber — ops director",
      "status": "escalation",
      "tone": "warning"
    },
    {
      "name": "Unassigned",
      "role": "Billing dispute owner",
      "status": "16 days",
      "tone": "danger"
    }
  ]
}

Editorial

The blocks that carry an argument rather than a number.

title

The opener for a part of a report: eyebrow, headline, standfirst, byline.

Account review · Week 33

Ripple Fiber is delivering on the build and slipping on everything around it

Construction is ahead of plan on all three live routes. The risk is administrative: replies are slower every week, and a billing dispute has now gone sixteen days without an owner.

Sentiment agent14 August 2026
{
  "type": "title",
  "eyebrow": "Account review · Week 33",
  "headline": "Ripple Fiber is delivering on the build and slipping on everything around it",
  "standfirst": "Construction is ahead of plan on all three live routes. The risk is administrative: replies are slower every week, and a billing dispute has now gone sixteen days without an owner.",
  "byline": "Sentiment agent",
  "dateline": "14 August 2026",
  "rule": true
}

heading

A heading inside a section. Levels 2–4, for structure a section boundary would over-state.

Part two

Where the time is going

{
  "type": "heading",
  "eyebrow": "Part two",
  "text": "Where the time is going",
  "level": 2,
  "rule": true
}

paragraph

One paragraph of plain text, capped at a readable measure. `lead` sets the opener.

Three of the four escalations this fortnight started as a question about an invoice and became a question about the build. That is the pattern worth breaking: billing friction is being read by the customer as delivery risk, and the delivery numbers do not support that reading.

{
  "type": "paragraph",
  "lead": true,
  "text": "Three of the four escalations this fortnight started as a question about an invoice and became a question about the build. That is the pattern worth breaking: billing friction is being read by the customer as delivery risk, and the delivery numbers do not support that reading."
}

prose

Markdown with GFM — tables, task lists, fenced code. `text` is accepted as the older name for this block.

Method

Sentiment is scored from three sources, weighted by how directly the customer speaks in them:

SourceVolumeWeight
Call transcripts230.5
SharePoint email2870.35
Ticket comments610.15

A message counts as an escalation signal when it contains a delay, cost or quality term within two sentences of a named route. Folder-level webUrls resolve via the app-only Graph token; clicking one still enforces your own SharePoint permissions.

  • Transcript ingest running daily
  • Email ingest running daily
  • eSpeed invoice feed — not connected
{
  "type": "prose",
  "title": "Method",
  "markdown": "Sentiment is scored from **three sources**, weighted by how directly the customer speaks in them:\n\n| Source | Volume | Weight |\n| --- | ---: | ---: |\n| Call transcripts | 23 | 0.5 |\n| SharePoint email | 287 | 0.35 |\n| Ticket comments | 61 | 0.15 |\n\nA message counts as an *escalation signal* when it contains a delay, cost or quality term within two\nsentences of a named route. Folder-level `webUrl`s resolve via the app-only Graph token; clicking one\nstill enforces your own SharePoint permissions.\n\n- [x] Transcript ingest running daily\n- [x] Email ingest running daily\n- [ ] eSpeed invoice feed — not connected"
}

callout

A note set apart from the copy. One knob — `tone` — and the icon follows from it.

{
  "type": "callout",
  "tone": "warning",
  "title": "Known gap in this report",
  "markdown": "eSpeed is **not connected**, so invoice figures come from the customer-supplied statement rather than our own ledger. Treat the value-at-risk total as a floor, not a measurement."
}

quote

The sentence from the transcript that made the case.

The crews are fine — we can see the routes going in. What we cannot see is who is answering the billing question, and it has been two weeks.
Dana Whitfield, Operations director, Ripple Fiber
Call transcript, 11 August 2026
{
  "type": "quote",
  "quote": "The crews are fine — we can see the routes going in. What we cannot see is who is answering the billing question, and it has been two weeks.",
  "attribution": "Dana Whitfield",
  "role": "Operations director, Ripple Fiber",
  "source": "Call transcript, 11 August 2026",
  "tone": "warning"
}

code

Code, a config fragment, a payload. Monospaced and unhighlighted; `highlight` shades lines.

Escalation rule
rules/escalation.jsonjson
1{
2 "terms": ["delay", "slipped", "chase", "dispute"],
3 "window_sentences": 2,
4 "requires_route_reference": true,
5 "min_confidence": 0.6
6}
{
  "type": "code",
  "title": "Escalation rule",
  "language": "json",
  "filename": "rules/escalation.json",
  "lineNumbers": true,
  "highlight": [
    4
  ],
  "code": "{\n  \"terms\": [\"delay\", \"slipped\", \"chase\", \"dispute\"],\n  \"window_sentences\": 2,\n  \"requires_route_reference\": true,\n  \"min_confidence\": 0.6\n}"
}

steps

A numbered procedure. `status` drives both the marker and the colour, so they cannot disagree.

Resolving the billing dispute
  1. Pull the three disputed invoices from the statement
    Done 05 Aug
  2. Reconcile against the as-built quantities
    Done 08 Aug
  3. Assign an owner and a resolution date
    Overdue

    This is the step everything else is waiting on — sixteen days unowned.

  4. 4
    Issue a credit note or a written rejection
{
  "type": "steps",
  "title": "Resolving the billing dispute",
  "steps": [
    {
      "title": "Pull the three disputed invoices from the statement",
      "status": "done",
      "meta": "Done 05 Aug"
    },
    {
      "title": "Reconcile against the as-built quantities",
      "status": "done",
      "meta": "Done 08 Aug"
    },
    {
      "title": "Assign an owner and a resolution date",
      "detail": "This is the step everything else is waiting on — sixteen days unowned.",
      "status": "blocked",
      "meta": "Overdue"
    },
    {
      "title": "Issue a credit note or a written rejection",
      "status": "todo"
    }
  ]
}

checklist

Things to be ticked off, with the count at the top. Unordered, where `steps` is ordered.

Pre-handover pack — Route 14
3 / 5
  • As-built GIS export lodgedUros
  • Splice matrix signed offUros
  • Chamber photos uploaded
  • Wayleave copies attached

    Two originals still with the landowner’s solicitor.

  • Reinstatement sign-offDue 20 Aug
  • Traffic management debrief

    Not required — footway works only.

{
  "type": "checklist",
  "title": "Pre-handover pack — Route 14",
  "items": [
    {
      "label": "As-built GIS export lodged",
      "state": "done",
      "meta": "Uros"
    },
    {
      "label": "Splice matrix signed off",
      "state": "done",
      "meta": "Uros"
    },
    {
      "label": "Chamber photos uploaded",
      "state": "done"
    },
    {
      "label": "Wayleave copies attached",
      "state": "blocked",
      "detail": "Two originals still with the landowner’s solicitor."
    },
    {
      "label": "Reinstatement sign-off",
      "state": "todo",
      "meta": "Due 20 Aug"
    },
    {
      "label": "Traffic management debrief",
      "state": "na",
      "detail": "Not required — footway works only."
    }
  ]
}

figure

An image with a caption and a credit. `image` is accepted as the older name.

Aerial view of a fibre route running alongside a carriageway

Figure 1Route 14 approaching the culvert at chainage 2100, where the as-built diverges 270 m from the quoted alignment.

Essentia GIS · survey flight 11 Aug 2026

{
  "type": "figure",
  "url": "https://essentia-inc.com/wp-content/uploads/2024/05/Broadband-v2-768x432.png",
  "alt": "Aerial view of a fibre route running alongside a carriageway",
  "label": "Figure 1",
  "figcaption": "Route 14 approaching the culvert at chainage 2100, where the as-built diverges 270 m from the quoted alignment.",
  "credit": "Essentia GIS · survey flight 11 Aug 2026",
  "aspect": "16/9",
  "fit": "cover"
}

footnotes

The small print at the end — method notes, caveats, exclusions.

Notes
  1. 1Response time is measured from the customer’s message to the first substantive reply, excluding auto-acknowledgements.
  2. 2Value at risk excludes the two credit notes issued on 06 August, which are settled but not yet reflected in the customer statement.
  3. †Route 21 figures are provisional pending the reinstatement inspection.
{
  "type": "footnotes",
  "title": "Notes",
  "notes": [
    {
      "text": "Response time is measured from the customer’s message to the first substantive reply, excluding auto-acknowledgements."
    },
    {
      "text": "Value at risk excludes the two credit notes issued on 06 August, which are settled but not yet reflected in the customer statement."
    },
    {
      "label": "†",
      "text": "Route 21 figures are provisional pending the reinstatement inspection."
    }
  ]
}

Structure

Layout, containers and punctuation.

html

Raw markup, sanitised at parse time and restricted to design-system classes. What is stored is already safe; what is not on the allowlists is dropped silently.

Wayleave clause 4.2, as written

Quoted verbatim from the landowner agreement — the nesting is theirs.

Grantor
Bridgend Estates Ltd
Term
25 years from first splice, break at 10
Access is permitted between 07:00 and 18:00 on weekdays, save that no works shall be carried out during lambing (01 Feb – 15 Apr) without fourteen days’ written notice.

Lambing window Route 14 chainage 1800–2100 only

{
  "type": "html",
  "title": "Wayleave clause 4.2, as written",
  "caption": "Quoted verbatim from the landowner agreement — the nesting is theirs.",
  "html": "<dl class=\"hb-grid hb-grid-2\">\n  <dt class=\"hb-key\">Grantor</dt>\n  <dd>Bridgend Estates Ltd</dd>\n  <dt class=\"hb-key\">Term</dt>\n  <dd>25 years from first splice, break at 10</dd>\n</dl>\n<blockquote>Access is permitted between 07:00 and 18:00 on weekdays, save that\nno works shall be carried out during lambing (01 Feb – 15 Apr) without\n<strong>fourteen days’</strong> written notice.</blockquote>\n<p class=\"hb-row\"><span class=\"hb-badge tone-warning\">Lambing window</span>\n<span class=\"hb-muted\">Route 14 chainage 1800–2100 only</span></p>"
}

embed

A third-party page in a sandboxed iframe (no same-origin access).

Embedded page
{
  "type": "embed",
  "title": "Embedded page",
  "url": "https://example.com/",
  "height": 200
}

divider

A labelled rule, or `dots` for a soft break between passages.

Detail
{
  "type": "divider",
  "label": "Detail"
}

spacer

Measured empty space, so air between parts never needs an empty card.

{
  "type": "spacer",
  "size": "lg"
}

columns

A split inside one block, for two things that belong together and must page-break together.

Why the diversion happened

The quoted alignment crossed a culvert that the desktop survey recorded as disused. It is not. The 270 m diversion was agreed on site on 22 July and is the single largest variance on the route.

Variance
Added duct
270 m
Added closures
2
Days impact
−3
{
  "type": "columns",
  "gap": "lg",
  "columns": [
    {
      "span": 7,
      "blocks": [
        {
          "type": "heading",
          "text": "Why the diversion happened",
          "level": 3
        },
        {
          "type": "paragraph",
          "text": "The quoted alignment crossed a culvert that the desktop survey recorded as disused. It is not. The 270 m diversion was agreed on site on 22 July and is the single largest variance on the route."
        }
      ]
    },
    {
      "span": 5,
      "blocks": [
        {
          "type": "fields",
          "title": "Variance",
          "inline": true,
          "fields": [
            {
              "label": "Added duct",
              "value": "270 m",
              "tone": "warning"
            },
            {
              "label": "Added closures",
              "value": "2"
            },
            {
              "label": "Days impact",
              "value": "−3",
              "tone": "positive"
            }
          ]
        }
      ]
    }
  ]
}

tabs

Peer panels the reader chooses between. Every panel prints, because a PDF has no tabs.

Escalations by route
  • Culvert diversion agreed on site
    22 Jul
  • As-built lodged, handover pack opened
    11 Aug

Complete and inspected. No open items.

{
  "type": "tabs",
  "title": "Escalations by route",
  "tabs": [
    {
      "label": "Route 14",
      "blocks": [
        {
          "type": "list",
          "items": [
            {
              "title": "Culvert diversion agreed on site",
              "meta": "22 Jul",
              "tone": "warning"
            },
            {
              "title": "As-built lodged, handover pack opened",
              "meta": "11 Aug",
              "tone": "positive"
            }
          ]
        }
      ]
    },
    {
      "label": "Route 17",
      "blocks": [
        {
          "type": "callout",
          "tone": "danger",
          "title": "Permit rejected",
          "markdown": "Highways rejected the section 50 on **6 August**. No resubmission has been filed."
        }
      ]
    },
    {
      "label": "Route 21",
      "blocks": [
        {
          "type": "paragraph",
          "text": "Complete and inspected. No open items."
        }
      ]
    }
  ]
}

accordion

Detail worth having and not worth reading first — the appendix.

Appendix
Full escalation log11 signals across the fortnight, newest first.
  1. 11 Aug
    Billing question repeated on the weekly call
  2. 06 Aug
    Section 50 rejected — Route 17
  3. 29 Jul
    Invoice dispute raised
Scoring weightsHow the grade on page one is arrived at.
Delivery
0.4
Responsiveness
0.35
Billing hygiene
0.25
{
  "type": "accordion",
  "title": "Appendix",
  "items": [
    {
      "label": "Full escalation log",
      "summary": "11 signals across the fortnight, newest first.",
      "blocks": [
        {
          "type": "timeline",
          "events": [
            {
              "when": "11 Aug",
              "title": "Billing question repeated on the weekly call",
              "tone": "danger"
            },
            {
              "when": "06 Aug",
              "title": "Section 50 rejected — Route 17",
              "tone": "danger"
            },
            {
              "when": "29 Jul",
              "title": "Invoice dispute raised",
              "tone": "warning"
            }
          ]
        }
      ]
    },
    {
      "label": "Scoring weights",
      "summary": "How the grade on page one is arrived at.",
      "tone": "neutral",
      "blocks": [
        {
          "type": "fields",
          "inline": true,
          "fields": [
            {
              "label": "Delivery",
              "value": "0.4"
            },
            {
              "label": "Responsiveness",
              "value": "0.35"
            },
            {
              "label": "Billing hygiene",
              "value": "0.25"
            }
          ]
        }
      ]
    }
  ]
}