/* Dominia — the cartography layer. Owner piece: CARTOGRAPHY.
   Target metrics live in REFERENCE-FACTS.md, read alongside the correction note in it:
   measured the same way as ours, Toner is 47.64% MARKED, 17 tones, zero chroma. We were 32.42%
   marked — sparser than the bar, not denser. Do not read the old "23.84% ink" figure as a cap.

   Nothing in this file mixes against a literal ground any more. Every tone is a color-mix()
   against the live --ink token, for the same reason js/cartography.js stopped hardcoding
   INK_RGB: the ground moved from a cool #0E1014 to a warm #15130E and every pre-computed
   rgb() literal here went on describing a page that no longer existed. A blind critic read the
   result off a screenshot — "neutral-grey roads out of temperature with the brown-black
   ground" — which is the failure mode a derived value cannot have. */

.carto {
  position: relative;
  background: var(--ink);
  overflow: hidden;
}

.cartography {
  width: 100%;
  height: 100%;
  display: block;

  /* Casing widths, next to the core widths they shadow (--road-* in css/tokens.css:
     motorway 5 · arterial 3.5 · secondary 2 · minor 1, from Toner's measured dark-run peaks).
     A casing is drawn underneath at the next class's tone, so a major road reads as a ribbon
     with a shoulder rather than as a line. Measured, the three casings add 8.2% of the frame
     at mid values — the band the board had almost nothing in. */
  --road-motorway-casing: 9px;
  --road-arterial-casing: 6px;
  --road-secondary-casing: 3.5px;
}

/* Strokes are `vector-effect: non-scaling-stroke`, so they are screen pixels whatever the
   viewport does — which means a road covers 3.7x more GROUND on a 390px phone than on a 1440px
   desktop (2000 board units across 390px is 0.195px per unit, against 0.72). Left alone, the
   majors swell into ribbons and swallow the minor-street mat that carries the texture. Scaled
   back by about a third here rather than in css/mobile.css, because this is the board's own
   geometry rather than a layout decision. Widths stay on Toner's measured 1 / 2 / 3.5 / 5
   proportions; only the unit shrinks. */
@media (max-width: 768px) {
  .cartography {
    --road-motorway: 3.4px;
    --road-arterial: 2.4px;
    --road-secondary: 1.4px;
    --road-minor: 0.7px;
    --road-motorway-casing: 6px;
    --road-arterial-casing: 4px;
    --road-secondary-casing: 2.4px;
  }
}

.road {
  fill: none;
  stroke-linecap: square;
  stroke-linejoin: miter;
  vector-effect: non-scaling-stroke;
  /* stroke colour is a solid tone baked per class in cartography.js — never stroke-opacity —
     so two roads crossing paint over one another instead of alpha-stacking into a stray tone. */
}

/* Casings meet at junctions constantly, so a mitre spike on a wide shoulder shows. Round the
   joins on the casing pass only; the cores keep the mitre that gives the network its hardness. */
.road--casing {
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Rail as a broken line, the way Toner draws it. A dash pattern is a stroke treatment, not a
   sixth colour, so this stays inside the palette. Drawn solid, the yard's 203 parallel sidings
   bundled into a grey smear instead of reading as track. */
.road--rail {
  stroke-dasharray: 5 4;
  stroke-linecap: butt;
}

/* ---------------------------------------------------------------- the block field
 *
 * This is the half of the hero the road network cannot do on its own.
 *
 * The bar's hero is a full-bleed aerial photograph and it works because of two smooth masses at
 * different values — deep water on one side, bright sand and forest on the other — with fine
 * fractal texture inside them. We have no photography and are not getting any, so the board has
 * to build that structure out of what it already knows.
 *
 * BUILT GROUND CARRIES NO FILL, and two attempts at giving it one are why that is written down.
 *
 * The first graded the fill by the density tier the pipeline assigns each block — urban
 * brighter, rural darker — on the reasoning that the tiers cluster geographically and would
 * give the frame a lit half and a dark half. Rendered, they do not: at this scale the tiers
 * interleave enough that the field read as light and dark patches scattered at random, i.e. as
 * speckle rather than as structure. The tier is still carried on the element as `data-tier`
 * because it is real data, and it is deliberately not painted.
 *
 * The second gave every block one tone, which is Toner's own construction — its measured third
 * tone, 12.88% of the tile, is block and casing fill. Two things killed it. It inverted the
 * board: at 20% slate the field became the brightest large mass in the frame and the open
 * spaces stopped reading as parks and started reading as holes punched in a light grey page,
 * which is the opposite of a city at night. And an opaque fill over 66% of the frame paints out
 * the falloff css/surface.css lays under the board, which is the surface lane's tonal work.
 *
 * So the board's third tone comes from the road CASINGS instead — 8.2% of the frame at mid
 * values, which is close to Toner's 12.88% and lands where the eye is already looking — and the
 * step between the board's plane and the page is the surface lane's, made by rebinding --ink to
 * --ink-2 inside `.carto`. Everything here mixes against that rebound token, so the board's
 * tones sit on the plane they are actually drawn on rather than on the page underneath it.
 */
.territory {
  /* Slate mixed 55% into the live ground, baked solid: same read as an alpha hairline, none of
     the compounding-tone cost of stroke-opacity where block edges overlap. */
  stroke: color-mix(in srgb, var(--slate) 55%, var(--ink));
  stroke-width: 0.6px;
  vector-effect: non-scaling-stroke;
  transition:
    fill var(--dur-ui) var(--ease),
    stroke var(--dur-ui) var(--ease);
}

/* Explicit, not omitted: SVG's initial fill is BLACK, so an unclaimed block with no rule is a
   solid black lozenge rather than open ground. */
.territory[data-state="unclaimed"] { fill: none; }

/* Open space — parks, lakes, tanks, the railway grounds — is a WELL, sunk below the board's
   plane. It used to be a hairline over nothing, which meant Cubbon Park, Lalbagh and the
   railway grounds, the three most recognisable shapes on this board, had exactly the same value
   as everything around them. Sinking them extends the board's range DOWNWARD, so the plane is
   no longer the floor, and gives the frame the large smooth dark masses that are the whole
   reason the reference hero reads as a photograph rather than as a texture.
   Mixed rather than set flat at --ink-0: against a plane at --ink-2 the raw floor punched holes
   that read as missing data. 70% of the way down is deep water; 100% was a hole.
   Still no fill COLOUR and still no marker — absence is the signal, as the brand specifies.
   This only gives absence a depth. */
.carto-openspace {
  fill: color-mix(in srgb, var(--ink-0) 70%, var(--ink));
  stroke: color-mix(in srgb, var(--slate) 50%, var(--ink));
  stroke-width: 0.6px;
  vector-effect: non-scaling-stroke;
}

/* ---------------------------------------------------------------- territory states
 *
 * Soft fills are the brand's territory tints BAKED SOLID against the live ground rather than
 * composited. An alpha fill sitting over several road tones yields several different resulting
 * colours; the pre-mixed equivalent yields one. That is the main reason our quantised tone
 * count once ran to 92 against Toner's 17. color-mix() keeps that property — it resolves to a
 * single opaque colour — while tracking the ground token instead of freezing a literal.
 *
 * The ratio moved from 0.14 to 0.30, and only because the field underneath moved. A 14% tint
 * against bare ink landed on rgb(48,26,17); the brightest neutral block now sits at rgb(47,45,38),
 * so at 14% a claimed territory would have been the same value as an unclaimed dense one and
 * colour would have stopped reading as an event. 30% clears the block field outright.
 * Scarcity is carried by AREA — 20 coloured blocks out of 205 — not by keeping colour faint.
 */
.territory[data-state="claim"] {
  fill: color-mix(in srgb, var(--claim) 30%, var(--ink));
  stroke: var(--claim);
}

/* Alliance and enemy are held at 12% against Claim's 30%, and it is a hierarchy rather than a
   saving. Five blind composition rounds on the hero all landed on the same sentence in different
   words: there is no single figure. With three territory colours at equal strength the board had
   three chromatic events of similar weight and the eye had no reason to prefer any of them —
   "two weak centres and no route between them".
   Claim is the one the page is about: it is YOUR ground, it is what the headline is promising,
   and it is where a route to the CTA has to begin. So it keeps full strength and the other two
   step down to a tint that still reads as held-by-someone without competing to be the subject.
   The STROKES stay at full colour. That is what preserves the meaning — a contested city still
   reads as contested at close range, and the three states are still tellable apart — while the
   FILL, which is what carries at a glance, belongs to one of them. */
.territory[data-state="alliance"] {
  fill: color-mix(in srgb, var(--alliance) 12%, var(--ink));
  stroke: var(--alliance);
}

.territory[data-state="enemy"] {
  fill: color-mix(in srgb, var(--enemy) 12%, var(--ink));
  stroke: var(--enemy);
}

/* Contested: the 2000ms ambient pulse, linear as specified — not ease-in-out. */
.territory[data-state="contested"] {
  fill: color-mix(in srgb, var(--claim) 30%, var(--ink));
  stroke: var(--claim);
  animation: contest var(--dur-ambient) linear infinite;
}

@keyframes contest {
  0%, 100% { fill-opacity: 1; }
  50%      { fill-opacity: 0.25; }
}

/* The one piece of positive tracking left on the site, on six SVG place labels. Flagged by the
   type builder against the bar's measured zero. Kept deliberately: tracked-out area labels are
   real cartographic convention — Toner, OS and every atlas set them that way — and untracked
   they stop reading as places and start reading as UI captions sitting on top of a map. */
.carto-label {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  fill: var(--slate-2);
  text-anchor: middle;
  dominant-baseline: middle;
  pointer-events: none;
}

/* ---------------------------------------------------------------- board fill ratios
 *
 * The strength of each state's fill ON THE SATELLITE PLATE, declared once and consumed twice:
 * here, by the board, and in css/hero.css, by the key that explains the board.
 *
 * They are tokens because the alternative was tried and failed silently. hero.css carried the
 * ratios as literals under a comment reading "The fills match the board's own territory fills
 * exactly - a key that does not is a lie", and by the time a blind critic looked at it the key
 * was on claim 30% / alliance 12% / rival 12% while the board had moved to 56 / 34 / 56. So the
 * two alliance and rival swatches rendered as near-hollow outlines against solidly filled cells,
 * and the critic reported the key as using "three different constructions describing one
 * rendering". It was not three constructions. It was one construction and stale numbers, which
 * is worse, because nothing about it looked wrong in the source.
 *
 * A comment cannot hold an invariant across two files. A token can. */
:root {
  --fill-claim: 56%;
  --fill-alliance: 34%;
  --fill-rival: 56%;
  /* Held back on purpose - see the contested note below. */
  --fill-contested: 32%;
}

/* ---------------------------------------------------------------- on satellite
 *
 * `.on-plate` is worn by every board drawn OVER THE SATELLITE — the hero, and each of the four
 * how-it-works beats. It was `.hero__map` while the hero was the only one; the beats used to
 * draw the vector board on flat ink, and the difference is most of why they read as a diagram of
 * a city rather than as a city. Nothing in this block was ever hero-specific: it is all "what a
 * vector board does differently when there is real terrain underneath it".
 *
   The board draws over a real satellite plate rather than over flat ink, and that changes
   what the road layer is FOR. On flat ink the roads WERE the map — they had to carry the whole
   city on their own, so they were drawn at Toner weights (5 / 3.5 / 2 / 1px) and near-white.
   Over imagery that is redundant twice over: the satellite already shows every street, and a
   5px near-white stroke on top of it reads as a smear, not as a road.

   So the network becomes what it should be on a game board — a thin vector overlay that says
   which lines the game cares about, with the terrain showing through. Weights roughly halve and
   the whole layer drops to 0.55 opacity, which lets the imagery read while keeping the graph
   legible enough to see that territories are bounded by real roads.

   The territory layer is deliberately NOT inside this opacity: it is the one thing on the board
   that must not be diluted. */
/* THE CASING PASS IS OFF OVER IMAGERY, and this is the single biggest thing that was wrong
   with the board.

   A casing is a wider stroke painted underneath a road to fake a shoulder. On flat ink that is
   necessary — there is nothing else there. Over a photograph it is not merely redundant, it is
   destructive: the plate already contains a real shoulder, in the right place, at the right
   width. What made it destructive here was that the `.on-plate` block below halved the CORE
   widths for imagery and never touched the casing widths, which are declared on `.cartography`.
   Probed on the rendered page, that left:

     motorway   casing 9px   rgb(126,121,112)   core 2.6px  rgb(145,141,132)
     arterial   casing 6px   rgb(127,121,111)   core 1.8px  rgb(126,121,112)
     secondary  casing 3.5px rgb( 99, 91, 79)   core 1.1px  rgb(127,121,111)

   The motorway casing is 3.5x its own core and 19 luminance steps from it; the arterial casing
   and its core are the SAME COLOUR to within 1/255. So every major road rendered as a wide flat
   pale band with no spine — fog laid over the city, not a road drawn on it — and because the
   band is wider than the real road beneath it, it read as being misregistered even though the
   geometry is exact.

   Off. The cores are then free to be what a vector overlay on terrain should be: thin, hard and
   brighter than the ground, so the network reads as DRAWN rather than as photographed. */
.on-plate .cartography .road--casing { display: none; }

/* With the fog gone the cores carry the whole network, so they come up from 0.55 — at that
   value a 0.6px minor street over lit terrain is below the plate's own noise floor. */
.on-plate .cartography .road { opacity: 0.9; }

.on-plate {
  --road-motorway: 2.2px;
  --road-arterial: 1.4px;
  --road-secondary: 0.9px;
  --road-minor: 0.5px;
}

/* ---- the board has to look like a board ----
 *
 * The page's central claim is that the city is divided into 312 real road-bounded territories
 * you can take one at a time. On the plate you could see twenty of them, because an unclaimed
 * block inherits `stroke: color-mix(--slate 55%, --ink)` — a dark hairline, designed for a dark
 * flat ground, laid over lit terrain where it is simply not there.
 *
 * So the hero board showed a satellite photograph with some red and green washes on it. The
 * grid — the product — was invisible, and the twenty coloured cells had nothing to be cells OF.
 *
 * A light hairline at low alpha fixes it, and it has to be light rather than dark for the same
 * reason the roads do: over imagery, an overlay reads as an overlay when it is brighter than
 * the ground. Kept genuinely faint. The cell field is meant to be legible as structure at a
 * glance and to survive inspection at close range — it is not meant to compete with the claim. */
.on-plate .territory[data-state="unclaimed"] {
  stroke: rgba(239, 235, 225, 0.26);
  stroke-width: 0.65px;
}

/* Open space is the board's large smooth dark mass, and at 0.35 it was not one.
 *
 * This is the half of the tonal structure the vector layer can supply that the grade cannot.
 * The reference hero gets its composition from two big smooth masses at clearly different
 * values with fine texture inside them; on this window the equivalent masses are real and
 * already in the data — Cubbon Park, Lalbagh, the racecourse, the railway grounds, the tanks.
 * Sunk properly they give the frame its dark half, and they are the shapes that make a
 * Bengaluru reader recognise the city. Still no fill colour and no marker: absence is still the
 * signal, it just has depth now. */
.on-plate .carto-openspace {
  opacity: 1;
  /* 62% was a hole. Over a satellite the park is ALREADY the dark, smooth, untextured shape in
     the frame — canopy and turf against rooftops — so the fill's job is not to build the mass
     but to deepen one that is really there. At 62% it painted the plate out entirely and the
     centre-left of the board became an amorphous ink spill with no texture inside it, which is
     the failure the reference photograph specifically does not have: its masses are smooth AND
     they carry fine detail. 28% sinks the open space clearly below the built fabric while the
     terrain still shows through it. */
  fill: color-mix(in srgb, var(--ink-0) 28%, transparent);
  stroke: rgba(239, 235, 225, 0.16);
}

/* Held ground has to read AS held ground against imagery, which is busier than flat ink was.
   Alliance and enemy were cut to 12% when the board sat on a flat field and three equal colours
   were fighting to be the subject; over terrain that reads as almost nothing, and a board where
   only your own ground is visible is not a contested city. 22% restores them as legible states
   while Claim, at 30% plus the contest pulse, stays the loudest thing on the plate. */
/* These ratios all moved together, and for one reason: the plate underneath them moved.

   The old figures were set against a plate graded to a median of L34. The grade now opens the
   midtones to a median near L60, so every tint that was reading against near-black is now
   reading against lit terrain — and a 34% wash of Claim over lit terrain is not a claim, it is
   pale salmon. The board's one saturated event had been quietly cancelled by making the ground
   underneath it brighter.

   So Claim comes up hard. It is the subject: it is the thing the headline is promising, and
   five blind composition rounds turned on it being strong enough to anchor a route down to the
   CTA. Alliance and rival stay a clear step below it — they are states, not the subject — but
   they too had to rise to stay legible on the new ground. The hierarchy is unchanged; only the
   ground it is measured against is. */
.on-plate .territory[data-state="alliance"] { fill: color-mix(in srgb, var(--alliance) var(--fill-alliance), transparent); }
/* Rival sits higher than alliance and it is not favouritism, it is chroma. `--enemy` is
   #4A6B8A, a muted slate blue, and the ground it has to survive is warm. Mixed at 34% over lit
   terrain near L60 it resolves to about rgb(91,100,105) — 14 points of chroma, which is under
   the threshold at which the hue registers at all. Measured by hue over the rendered hero, the
   ten rival cells were on frame, correctly placed, and came to 0.01% of it: rendering, and
   invisible. Alliance green and Claim red are both far enough from a warm grey to hold at 34%;
   a cool blue is not, because the warm ground neutralises it. 56% is what it costs to make the
   same statement the other two make at 34%. */
.on-plate .territory[data-state="enemy"]    { fill: color-mix(in srgb, var(--enemy) var(--fill-rival), transparent); }
.on-plate .territory[data-state="claim"] { fill: color-mix(in srgb, var(--claim) var(--fill-claim), transparent); }

/* The contested cell is deliberately HELD BACK from the claim's 56%, and this is the one place
   on the board where a value difference carries a rule rather than a preference.

   Raising claim to 56% cost the claim route its separation: the route is stroked in the same
   Claim red as the fill it now sits on, so the block being walked and the blocks already held
   became one flat red mass and the only thing on the hero that is HAPPENING disappeared into
   the things that are merely true.

   Holding the contested cell down fixes it, and says the right thing while doing so. A cell you
   are still walking is not yet yours. So it carries the outline at full strength and only a
   trace of the fill, and the fill is what arrives when the walk closes. Held ground is solid;
   ground being taken is drawn. That is the difference the route is there to show, and now the
   board states it without needing the HUD to explain it. */
/* 22% -> 32%. Two independent blind critics read a "pale rectangular patch that follows no
   real parcel edge" inside this cell and both called it a raster registration artefact. Checked
   against the source plate, it is neither an artefact nor misregistered: it is a real open
   parade ground, and with the plate blur removed you can now see linear features in it. So the
   diagnosis was wrong — but the reading was not, and the reading is what a visitor gets. The
   cause is that this is the ONLY cell on the board carrying a border and a near-transparent
   fill, so it is the only place where raw imagery is on full display inside a figure the eye
   has been directed to; every held cell around it is unified by a 56% tint. 32% compresses the
   interior's range by about a third while leaving the route — a solid stroke at full Claim —
   clearly separated from both the 56% held cells and the ground. */
.on-plate .territory[data-state="contested"] { fill: color-mix(in srgb, var(--claim) var(--fill-contested), transparent); }

/* Strokes carry the state's identity at close range and must not be diluted by the plate.
   Held cells are now also the only FULLY OPAQUE edge on the board — the unclaimed field above
   is a 26% bone hairline — so held and unheld differ in edge as well as in fill, and a held
   cell still reads as held where the fill happens to cross bright terrain. */
.on-plate .territory[data-state="claim"],
.on-plate .territory[data-state="contested"],
.on-plate .territory[data-state="alliance"],
.on-plate .territory[data-state="enemy"] { stroke-width: 1.4px; }

/* ---------------------------------------------------------------- the live claim
 *
 * The board's half of the claim-in-progress panel. Geometry and progress come from js/hud.js;
 * this is only how it looks.
 *
 * The route traces the block's real perimeter — the shape you walk to take it — and draws from
 * zero to closed in step with the ring in the panel. Claim red, because this is the one thing on
 * the hero that is happening rather than merely being true.
 */

.claim__route {
  fill: none;
  stroke: var(--claim);
  stroke-width: 2.4px;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* Non-scaling so the line holds its weight under the hero's 1.18 crop; without it the route
     thins as the board scales and stops reading as drawn. */
  vector-effect: non-scaling-stroke;
  /* dasharray and dashoffset are written by js/hud.js — the draw IS the progress value. */
}

/* The mark: the brand's square, on the block being taken. It breathes on the 2s ambient beat,
   which is the duration this brand reserves for things that are alive rather than things that
   are transitioning. Scale rather than opacity — a mark that fades out is a mark that is
   sometimes absent, and the point is that it is there the whole time. */
.claim__mark {
  fill: var(--claim);
  transform-box: fill-box;
  transform-origin: center;
  animation: claim-breathe var(--dur-ambient) ease-in-out infinite;
}

@keyframes claim-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.55); }
}

/* The block's real name, which is the whole argument of the section it sits in. It is the only
   label left on the hero board — the rest were cut because they fought the headline — and this
   one earns its place by being the subject rather than scenery. */
.claim__label {
  font-family: var(--font-mono);
  font-size: 15px;
  letter-spacing: 0.06em;
  /* Bone, not Claim red. The route, the mark and the block fill already carry the state — a
     fourth red element on the same 150px of board was the colour doing a job it had already
     done three times, and the name is information rather than status. */
  fill: var(--bone);
  dominant-baseline: middle;
  paint-order: stroke;
  /* Same hard halo the hero's other on-plate chrome uses: terrain under a label runs from roof
     shadow to lit concrete, and a stroke is what carries type across that. */
  stroke: var(--ink);
  stroke-width: 3px;
  stroke-linejoin: round;
}

/* On the phone the label has to belong to its cell, and at 15px it did not.
   Measured at 390x844 it rendered 177px wide against a contested cell 45px wide — four times the
   width of the thing it names, centred on it and overhanging both sides — which is why a blind
   critic read it as "an orphan label floating unattached", a leftover basemap tile label rather
   than a designed element. (The same critic placed it 100px lower than it actually sits and
   called it detached; measured, it is 9px below the cell. The reading was right, the evidence
   was not — the label is not detached, it is oversized.)
   11px with the tracking eased brings it to about 120px. It still overhangs, because the cell is
   small and the name is long, but it now reads as a caption on a parcel rather than as map
   furniture in its own right. */
@media (max-width: 900px) {
  .claim__label {
    font-size: 11px;
    letter-spacing: 0.04em;
    stroke-width: 2.5px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .claim__mark { animation: none; }
}

/* ---- secured ----
   The shared completion beat. The route and the ring reach 1.0 together — measured to within
   0.001 — but a closed loop reads as finished before a ring does, so the landing is marked
   explicitly rather than left to be inferred. The line thickens, the mark stops breathing and
   sits solid, and the label brightens: the claim is no longer in progress, it is held. */
/* ...and when the walk closes, the cell fills. That half is NOT here: js/hud.js sets the block's
   own data-state to `claim` at completion, so it takes the held fill from the state rules above.
   It used to be a rule in this spot keyed on `.is-secured .territory`, which could never match —
   `.claim` is a sibling of the territory layer, not an ancestor of it. See attachClaimRoute(). */

.claim.is-secured .claim__route { stroke-width: 3.2px; }
.claim.is-secured .claim__mark  { animation: none; transform: scale(1.25); }
.claim.is-secured .claim__label { fill: var(--bone); }
