Skip to content
Mapping

Paint & selection

The colours the map paints planning content with, and the interaction states that spend no colour at all. Numbers measured from the shipping map.

Assessment Ramps

0 Not yet mapped
1 No approval
2 Building approval
3 Council approval (code)
4 Council approval (impact)
The assessment rampTier 0 is neutral by obligation: an unassessed parcel must not borrow a colour that asserts something.
Code
import { tierMatchExpression, fabric } from "@gridd/ui/cartography"

map.addLayer({
  id: "lots-fill",
  type: "fill",
  source: "lots",
  paint: {
    "fill-color": tierMatchExpression("tier"),  // baked integer 0–4 → colour
    "fill-opacity": 0.62
  }
})

// In a TEMPLATE, never import from here — use var(--gd-cartography-*),
// so a theme change stays a theme change.

The Ramps

cartography
--gd-cartography-assessment-0#B9BCC4#B9BCC4

tier 0 — not codified, or not applicable. Neutral by obligation: an unassessed parcel must not borrow a colour that asserts something

--gd-cartography-assessment-1#1B8A4C#1B8A4C

tier 1 — no approval needed. Equals --gd-success TODAY BY COINCIDENCE; never alias it

--gd-cartography-assessment-2#2F6FB0#2F6FB0

tier 2 — building approval (a certifier, not council). Coincides with --gd-info; never alias it

--gd-cartography-assessment-3#C9A21A#C9A21A

tier 3 — council approval, code assessed. DOES NOT equal --gd-caution (#A8850F): aliasing would darken every code-assessable parcel

--gd-cartography-assessment-4#C43D3D#C43D3D

tier 4 — council approval, impact assessed. Coincides with --gd-danger; never alias it

--gd-cartography-assessment-0-line#696D77#B9BCC4

tier 0 casing — the darker stroke for an unassessed parcel

--gd-cartography-assessment-1-line#15703D#5BD08B

tier 1 stroke — also the pass outline in the 3D scene, where a 2.2px line needs the ink rather than the fill

--gd-cartography-assessment-2-line#285F96#7FB3E6

tier 2 stroke

--gd-cartography-assessment-3-line#7F6508#EAB94D

tier 3 stroke

--gd-cartography-assessment-4-line#A83232#E97B7B

tier 4 stroke — the fail outline in the 3D scene

--gd-cartography-parcel-line#C2C8D2#C2C8D2

the cadastre hairline between parcels. At 0.5px over every parcel in view this is the highest-frequency line on the map, so it sets the texture of the whole fabric — dark enough to read the subdivision pattern, light enough never to compete with a road or the fill beneath it

--gd-cartography-building#E8E9ED#E8E9ED

existing building massing, extruded from LiDAR. A warm near-white, not pure white: the layer is semi-transparent so a parcel's fill still reads through, and pure white picks up a cold tint from whatever is beneath it

--gd-cartography-massing#17A6A0#17A6A0

GRIDD's generated best-fit massing

--gd-cartography-massing-alt#0F7D78#0F7D78

the alternating storey shade, so a stack of levels is legible rather than one fused block

--gd-cartography-easement#8A7AB5#8A7AB5

a registered easement's fill. A title fact, not a hazard — which is what the dashed edge says

--gd-cartography-easement-line#6B5A9E#6B5A9E

the easement's dashed edge

--gd-cartography-basemap-ground#F8F7F7#22242B

the basemap ground plane — the paper the cadastre is drawn on

--gd-cartography-basemap-road#BCC8D5#3A3F4A

road fill. Deliberately heavier than the cadastre hairline: a road is a place you can go, a boundary is not

--gd-cartography-basemap-park#FFFFFF#282B33

parks and reserves. White in the light, NOT green: a green basemap park collides with the zone palette, which already uses light greens to mean specific things (sport & recreation, rural, open space). Basemap green under those reads as a zone the lot does not have

--gd-cartography-basemap-water#A2D8EC#1E3A47

rivers, creeks and the bay — the one basemap feature that stays recognisably itself in both themes

--gd-cartography-basemap-label#5C6470#9AA3B0

street names — the basemap's working label

--gd-cartography-basemap-label-strong#4A525E#C2C9D4

suburb and town names — the label that anchors the view

--gd-cartography-basemap-label-halo#FFFFFF#101116

the halo behind a basemap label. It is the GROUND's colour, not a colour of its own: its whole job is to punch the label's own paper out from under it where the label crosses the parcel fabric

--gd-cartography-utility-water#12809C#12809C

water main

--gd-cartography-utility-sewer#8A6A2B#8A6A2B

sewer main

--gd-cartography-utility-storm#708B96#708B96

stormwater main

--gd-cartography-seq-1#E8EEF6#E8EEF6

sequential ramp, lowest class — a number's smallest values. Means MORE-vs-less and nothing else; never an assessment tier

--gd-cartography-seq-2#B9CFE6#B9CFE6

sequential ramp, second class

--gd-cartography-seq-3#7FA9CE#7FA9CE

sequential ramp, middle class

--gd-cartography-seq-4#4A7FAF#4A7FAF

sequential ramp, fourth class

--gd-cartography-seq-5#1F4E79#1F4E79

sequential ramp, highest class. The five steps fall monotonically in lightness, so the order survives greyscale and colour-vision deficiency

--gd-cartography-cat-1#3F7CAC#3F7CAC

categorical fill 1 — one colour per value, no order implied

--gd-cartography-cat-2#C1663F#C1663F

categorical fill 2

--gd-cartography-cat-3#4E9A6A#4E9A6A

categorical fill 3

--gd-cartography-cat-4#8C6BB1#8C6BB1

categorical fill 4

--gd-cartography-cat-5#B8983F#B8983F

categorical fill 5

--gd-cartography-cat-6#6E7B8B#6E7B8B

categorical fill 6 — the last one; a seventh category wants grouping, not another hue

--gd-cartography-cat-other#C2C8D2#C2C8D2

everything past the sixth category, as one neutral 'other' class. Neutral by obligation, exactly like assessment-0: a bucket must not borrow a colour that asserts it is a value

Reading Tokens in Paint Specs

tierFill(tier)
0#B9BCC41#1B8A4C2#2F6FB03#C9A21A4#C43D3D
tierLine(tier)
0#696D771#15703D2#285F963#7F65084#A83232
fabric()
parcelLine#C2C8D2building#E8E9ED
massing()
a#17A6A0b#0F7D78
services()
easement#8A7AB5easementLine#6B5A9Ewater#12809Csewer#8A6A2Bstorm#708B96
Called at renderLight values. Theme is a parameter, not a second palette: tierLine(4) is #A83232 and tierLine(4, "dark") is #E97B7B.
Code
import { tierFill, tierLine, fabric, services, basemap } from "@gridd/ui/cartography"

tierFill(3)                 // the light value
tierLine(4, "dark")         // theme is a PARAMETER, not a second palette
fabric().parcelLine
services().easementLine
basemap("dark").ground      // the one group that fully forks

// token() throws on an unknown name rather than returning a fallback:
// a silent fallback in a paint spec is a wrong colour nobody notices for a month.

The tier match

["match",["get","tier"],0,"#B9BCC4",1,"#1B8A4C",2,"#2F6FB0",3,"#C9A21A",4,"#C43D3D","#B9BCC4"]

That is the real output of tierMatchExpression("tier"), built in the package rather than at the call site.

Selection and Hover

Rest
Hover
Selected
The three statesRest · hover (2 px ink) · selected (2.6 px accent). The fill is identical in all three — that is the rule.
Code
import { token } from "@gridd/ui/tokens"

map.addLayer({
  id: "lots-hover", type: "line", source: "lots", "source-layer": "lots",
  paint: {
    "line-color": token("--gd-ink", theme),
    "line-width": ["case", ["boolean", ["feature-state", "hover"], false], 2, 0]
  }
})

map.addLayer({
  id: "lots-selected", type: "line", source: "lots", "source-layer": "lots",
  paint: {
    "line-color": token("--gd-accent", theme),
    "line-width": ["case", ["boolean", ["feature-state", "selected"], false], 2.6, 0]
  }
})

// The FILL is untouched in both. That is the rule.
  • Hover — a 2 px stroke in --gd-ink, driven by feature-state.
  • Selected — a 2.6 px stroke in --gd-accent (#4740DB light, #5F57E3 dark).
  • The cursor is the third signal — pointer over a parcel, default elsewhere, hit-tested on the same layer the hover strokes.
  • The search focus is a separate thing — a suburb result gets a 0.05 accent wash and a dashed 2.5 px outline; a city result gets the outline only, with the wash at 0.

Constraints

Zoning 0.62
Easement 0.35
Footprint 0.32
Focus wash 0.05
The opacity ladderOne hue at the map's own opacities, over the basemap ground — zoning is the loudest thing, a wash is barely there.
Code
import { services } from "@gridd/ui/cartography"

// a registered easement: a TITLE fact, and the dashed edge is what says so
map.addLayer({
  id: "easement-fill", type: "fill", source: "easements",
  paint: { "fill-color": services().easement, "fill-opacity": 0.35 }
})
map.addLayer({
  id: "easement-line", type: "line", source: "easements",
  paint: {
    "line-color": services().easementLine,
    "line-width": 1.6,
    "line-dasharray": [2, 1.6]
  }
})

// constraint overlays take their opacity from the group catalogue (0.14–0.28),
// declared once per group rather than per layer.
  • Zoning — 0.62. The loudest fill on the map.
  • Constraint overlays — a per-group fill opacity between 0.14 and 0.28, declared once in the overlay catalogue. Flood is 0.28, bushfire 0.22, character housing 0.14.
  • Registered easements — 0.35 fill with a dashed 1.6 px edge ([2, 1.6]).
  • The regional urban footprint — 0.32, and it exists only below the council gate.
  • Subdivision cells — alternating 0.16 / 0.28 by index parity.

Basemap Layers

LightDark
The paperThe plane the cadastre is drawn on. Charcoal in the dark, not black: the fills above it are translucent and were tuned against a light-ish ground.
RoadsDeliberately DARKER than the ground. Upstream Positron draws white roads on near-white land — about 1.05:1, effectively invisible — and inverting that polarity is the whole reason the retune exists.
ParksWhite in the light, not green. A green basemap park collides with the zone palette, which already uses light greens to mean specific things; green under those reads as a zone the lot does not have.
WaterThe one basemap feature that stays recognisably itself in both themes.
Street namesPositron's own ink is ~3.3:1 on this ground, under the floor. This clears 4.5:1 on the ground token above.
Suburb namesThe label that anchors the view — the darker of the pair in the light theme, the lighter in the dark. In both, the one that wins.
Label haloThe GROUND's colour, not a colour of its own: its job is to punch the label's own paper out from under it where it crosses the parcel fabric.

Rules

  • Never alias a cartography token to a status token. Not tier 1 to --gd-success, not tier 4 to --gd-danger, however exactly they match today.
  • Use these tokens for fills and strokes only — geometry on a canvas, and the legend swatch that describes it. Never a badge, a border, a chip, a row or a sentence.
  • In a template, use var(--gd-cartography-*). Never import from @gridd/ui/cartography into a template.
  • In a paint spec or a three.js material, use the accessors. var() inside a MapLibre paint value is a parse error, not a colour.
  • Build the tier expression with tierMatchExpression, never inline — the fallback has to be tier 0.
  • Keep the interaction states colourless. Hover and selection are strokes; the fill never changes.
  • Do not put a wash over a whole LGA. A city-scale focus gets the dashed outline and nothing else.
  • Change theme with setPaintProperty, never setStyle.
  • Guard every third-party layer id, and count the misses. Report when more than half are gone.
  • Keep a publisher's own fills out of the design system. A council's zone palette belongs in the spec's colour registry, with its provenance.

Behavior & Anatomy

Zoning fills sit at 0.62 over the light basemap: strong enough that the colour IS the map, weak enough that boundaries and labels survive on top of it. That leaves nothing for interaction to spend, which is why hover and selection are lines. A hover fill would sit on top of the zoning colour — the one thing the map exists to show.

The coincidences are coincidences

Tiers 1, 2 and 4 equal --gd-success, --gd-info and --gd-danger today and are deliberately NOT aliased to them — aliasing would re-couple a red error toast to an impact-assessable parcel. Tier 3 proves it: it is #C9A21A while --gd-caution is the darker #A8850F (promoted because #C9A21A is 2.41:1 on white and fails as a non-text mark), so an alias would silently darken every code-assessable parcel on every map we serve.

Tiers are baked; colours are not

The pipeline writes an integer 0–4 per parcel and the tier→colour step is a runtime match. Re-tuning the palette costs a rebuild, never a re-bake of a council's tiles — which is the difference between a colour decision taking an afternoon and taking a week.

Published fills — a council's zone palette, the flood bands, ShapingSEQ's footprint — are not in the design system. They are facts about a publisher and live in the spec's colour registry with their provenance attached.

Why there is an accessor and not a second copy

A MapLibre paint spec cannot read a CSS custom property: var(--gd-cartography-assessment-3) inside one is a parse error, not a colour. So the map needs the palette as JavaScript — but every accessor resolves through token(), the same model that generates tokens.css. There is one source and this is an accessor, not a mirror, which is what stops the canvas and the stylesheet disagreeing.

token() throws on an unknown name rather than returning a fallback, because a silent fallback in a paint spec is a wrong colour on a map that nobody notices for a month. tierMatchExpression is built in the package for the same class of reason: the fallback matters, and an unbaked or unknown tier must land on tier 0's neutral rather than on a colour that asserts an assessment nobody computed.

The fills are the same value in both themes on purpose. --gd-surface-map is charcoal rather than black precisely so a parcel's fill keeps the relationship to its ground that it has in daylight. Only the strokes fork, because a hairline has to hold against its own backdrop.

The focus wash, and why a city gets no fill

A suburb search result gets a 0.05 accent wash — so faint it is presence rather than paint — and a dashed outline. A CITY result gets the outline only, with the fill explicitly set to 0: a 5% indigo wash over an entire LGA reads as a hazard overlay, and gets mistaken for council-wide flood. The dashed boundary alone frames the council.

Note what this means for the selected PARCEL: it has no wash at all. Hover and selection are strokes and nothing else, and the 0.05 in the ladder above belongs to search focus, not to selection.

Why overlays are translucent and easements are dashed

Two hazards overlapping must both stay legible, and the darker sum is the intersection — which is only true while each layer is faint enough to be seen through. The catalogue sets each group's opacity once, between 0.14 and 0.28, so a new hazard inherits the behaviour instead of picking a number beside it.

A registered easement is a TITLE fact, not a hazard, and the dash is what says so. The regional urban footprint sits at 0.32 and exists only below the council gate — context for choosing a council, never competing with that council's own layers. Subdivision cells alternate 0.16 / 0.28 so adjacent proposed lots separate without introducing a second hue.

The layers panel's swatches take the map's ACTUAL colours — data, not tokens — which is why those literals are marked gd-colour-ok in the docs rather than tokenised (Layers panel).

The basemap is the one thing that should change when the lights go out

The shipped map repaints CARTO Positron rather than using it as it comes. The seven values are returned together by basemap(theme) on purpose: they are a measured set, not seven independent choices. Swap one and you have broken the pair it was measured against.

  • Content does not fork; the paper does. Every value above this section holds across themes so an assessment tier means the same thing at midnight as at noon. The basemap is the deliberate opposite.
  • Never setStyle to change theme. It tears down every source and layer — the parcel fabric, the selection, the 3D scene — and reloads the tiles behind them. setPaintProperty swaps the colours in place and the map does not blink.
  • Every layer id is third-party, not an API. CARTO can rename them upstream, so each call is guarded — but a silent degrade to plain Positron reads as a styling whim rather than a broken dependency, so the app counts the misses and reports when more than half are gone.

Navigate

Esc