/* ---------------------------------------------------------------------------------------------
   SELF-SUFFICIENCY BLOCK -- rules this suite needs that used to live only in the PARENT SITE.
   Added 2026-08-14 after dev.hawsedc.com's first deploy came up with no blue form backgrounds and
   no table borders.

   WHAT HAPPENED. Every page loads three stylesheets: bootstrap, this file, and `/hawsedc.css` --
   and that third one sits at the SITE ROOT and is NOT tracked by the engcalcs repository. It
   belongs to hawsedc.com. Deploy engcalcs on its own and it simply is not there, so `form`
   backgrounds and table borders vanish while everything else looks fine. 464 bytes, outside the
   repo, invisible to every check we have.

   WHY IT MATTERS BEYOND ONE DEV BOX: LibreEPANET.org (ROADMAP Task 306) is BY DEFINITION a
   standalone deploy of this suite on another domain. It would have walked into exactly this, and
   the symptom -- most of the page fine, some styling missing -- is the kind that gets blamed on a
   cache for a day before anyone looks at a 404.

   DUPLICATION IS DELIBERATE AND SAFE. On hawsedc.com `/hawsedc.css` still loads LAST, so its
   identical declarations win and nothing changes there. On a standalone deploy these are the only
   copy. Kept at the TOP of this file so every engcalcs rule below can still override them, which is
   how they behaved when they were in a stylesheet loaded before... nothing. Keep the VALUES in step
   with `/hawsedc.css` if that file is ever edited.
   --------------------------------------------------------------------------------------------- */
body { font-family: sans-serif; }
form {
	background: #f7f9ff;
	border: 1px solid #66c;
	margin-bottom: 1px;
}
.left { float: left; margin: 0.5em; }
table, th, tr, td {
	border: 1px solid blue;
	border-collapse: collapse;
	padding: 0px 2px 0px 2px;
}
/* "bare" tables and their immediate children show no border -- the layout tables the calculator
   forms are built from. Without this every calculator is a grid of blue boxes. */
table.bare, table.bare > tbody > tr, table.bare > tbody > tr > td { border-width: 0px; }

/* **INPUTS AND RESULTS START AT THE SAME TOP EDGE** (Tom, 2026-08-24: on a narrow screen they are
   "vertically aligned middle, and this is not good UX... both are initially visible from the top of
   the page"). A <td> is middle-aligned by the UA stylesheet, so the SHORTER of the two columns
   floats down the taller one -- and the narrower the window, the taller the Inputs column grows and
   the further Results sinks below the fold.

   Unconditional rather than behind the 640px breakpoint on purpose: the two columns are rarely the
   same height at ANY width, and top is the right answer whenever they differ. One rule beats a rule
   plus an exception. Scoped to `table.bare`'s own cells so it cannot reach a results table nested
   inside one. */
table.bare > tbody > tr > td { vertical-align: top; }

/* The calculator's input lines (ROADMAP Task 478, echoInputGrid() in lib/Calculators.lib.php).
   The DOM is COLUMN-MAJOR -- every input, then every label, then every unit select, then every X
   -- so Tab walks the input column instead of crossing each line sideways. Each cell carries its
   own `grid-row:N`, which puts it back on the line the user sees, so this must reproduce what the
   <table> above draws, to the pixel -- and does, wherever the form fits the window. Where it does
   not fit, one thing differs and cannot be made not to; echoInputGrid()'s docblock says which:

     - `auto` tracks, because an auto track sizes to max-content and shrinks toward min-content
       when the container is narrow, which is what a table's auto layout does. `max-content` would
       stop the labels wrapping on a narrow screen and turn the wrap into a horizontal scroll.
     - `justify-content: start`, or auto tracks would STRETCH to fill the cell and push the X
       column to the far right; a table shrink-wraps.
     - THE CELL STRETCHES AND ITS CONTENT CENTRES, which is why there is an inner `.ec-fg-in`
       and not just `align-items: center` on the grid. A <td> is middle-aligned (UA stylesheets
       put `vertical-align: middle` on the row group and a cell inherits it), but it also fills
       the row's height, and the bottom border is drawn on the FULL cell. Centring the grid items
       themselves would shrink every short cell to its own content, and on a line whose label
       runs to two -- Manning Trap's flow depth, with the solver control under it -- the blue
       rule would break into four pieces at four different heights. So the cell keeps the
       default `stretch`, draws the rule at the row's true bottom, and a flex centre inside it
       puts the content back where a table put it.
     - 1px blue rules, collapsed: the container draws the top and leading edges, every cell draws
       its own trailing and bottom edge, so no two cells ever double one line. `.ec-fg-input`
       draws NO trailing edge -- the input and its unit select shared one <td> before this and
       must not gain a rule between them.
     - LOGICAL properties throughout (block-start/inline-start), so ar/he/fa/ur/ps get the mirror
       image without a second rule. `grid-column: 1` is already the leading column in RTL.

   A hidden line is `.collapse` without `.show` on all four of its cells at once (the X is a
   multi-target collapse), which removes them from the grid entirely, so the row they shared
   collapses to zero height with no gap -- what `display: none` on a <tr> did. */
.ec-fieldgrid {
	display: grid;
	grid-template-columns: auto auto auto auto;
	justify-content: start;
	border-block-start: 1px solid blue;
	border-inline-start: 1px solid blue;
}
/* ONE CLASS, NO DESCENDANT COMBINATOR, and that is load-bearing: Bootstrap hides a collapsed line
   with `.collapse:not(.show) { display: none }`, specificity (0,2,0). A `.ec-fieldgrid > .ec-fg-cell`
   selector ties that and would win on source order, leaving every "hidden" line on screen. At
   (0,1,0) Bootstrap wins whatever order the two stylesheets load in. */
.ec-fg-cell {
	display: flex;
	align-items: center;
}
/* Written by echoLineMirrorScript() onto the other three cells of a line whose input cell a page's
   own script has hidden by id. AFTER .ec-fg-cell on purpose -- the two tie on specificity, so
   source order is what decides, and an inline style (which is what "View printable" writes) still
   outranks both. */
.ec-line-off { display: none; }
.ec-fieldgrid > .ec-fg-cell {
	padding: 0px 2px 0px 2px;
	border-block-end: 1px solid blue;
	border-inline-end: 1px solid blue;
}
.ec-fieldgrid > .ec-fg-cell > .ec-fg-in { min-width: 0; }
.ec-fieldgrid > .ec-fg-label { grid-column: 1; }
/* NO RULE AND NO PADDING AT THIS ONE SEAM. The input and its unit select shared a single <td>
   before Task 478, separated by one space character, so the column boundary between them must
   draw nothing and add nothing: echoInputGrid() puts a real no-break space at the end of the
   input cell instead, which is the same glyph advance in any font and any language. 2px + 2px of
   padding was the obvious substitute and it is measurably wrong -- it narrowed the whole form by
   1.1px and moved every unit select and every result on the page with it
   (dev/browser-pass/fieldgrid-layout.js measured it against the <table> version). */
.ec-fieldgrid > .ec-fg-input { grid-column: 2; border-inline-end: 0; padding-inline-end: 0; }
.ec-fieldgrid > .ec-fg-units { grid-column: 3; padding-inline-start: 0; }
.ec-fieldgrid > .ec-fg-x     { grid-column: 4; }

.input {
	width: 7em;
}
/* Wave 0 narrow-column mechanism (validated 2026-07-06): holds a results-table
   header as narrow as the old hard <br /> breaks did, so breaks can be removed
   from lang strings for clean translation. Wrap a header label in this span and
   the column stays fixed-width while long/other-language text wraps. Class-scoped
   so it never touches ip_/wi_ which share #CalcsTable. Default is deliberately
   NARROW for no-drop-down columns (nothing sets a width floor there, so keep them
   tight). Mid-word wrapping across the 26 languages is expected and accepted — do
   NOT widen to make English wrap prettily (that only helps English). Override per
   column with an inline width where a unit-select sets a wider floor. */
.ec-narrowcol {
	display: inline-block;
	width: 3.6em;
	overflow-wrap: break-word;
	white-space: normal;
	vertical-align: top;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
	-webkit-appearance: none;
	margin: 0;
}

/* Firefox */
input[type=number] {
	-moz-appearance: textfield;
}

/* Opt back IN to the browser's native spinner arrows, per input (Tom, 2026-07-30, on the map-label
   decimal-places field). The two rules above strip the arrows from every number input in the suite
   -- right for a physical quantity, where the arrows are useless (a 1-unit step means nothing to a
   diameter or a flow) and only steal width from the number. It is wrong for a SMALL BOUNDED INTEGER
   like "decimal places, 0-4", where clicking up/down is the natural gesture and the arrows are what
   tell the user at a glance that this field is not a free-form number. Add .ec-spin to such a field.
   opacity:1 is not redundant: Chrome/Safari hide the spinner until the pointer is over the input, so
   without it the control is invisible exactly when the user is looking for it. */
input.ec-spin::-webkit-outer-spin-button,
input.ec-spin::-webkit-inner-spin-button {
	-webkit-appearance: auto;
	appearance: auto;
	opacity: 1;
	margin: 0;
}
input.ec-spin[type=number] {
	-moz-appearance: auto;
	appearance: auto;
}

.blink {
  animation: blinker 1s step-start 15;
}

@keyframes blinker {
  50% {
    opacity: 0;
  }
}

/* Validity/status check results (velocity checks, regime checks, DU quality,
   D50 range checks, head-loss checks, conveyance efficiency, etc.) */
.ec-status-ok {
	color: #267326;
}
.ec-status-info {
	color: steelblue;
}
.ec-status-warn {
	color: #c60;
}
.ec-status-bad {
	color: #c00;
}
.ec-status-neutral {
	color: gray;
}

/* Branched-Network per-row topology verdict. It sits in the ID or Upstream cell of the
   line that is actually wrong, under the input rather than beside it, so a narrow column
   does not have to grow to hold it. Replaced a page-level banner above the table (Tom,
   2026-09-06: "out of place and confusing"). */
.bpn-rowflag {
	display: block;
	color: #c60;
	font-size: 11px;
	white-space: nowrap;
}

/* The engine-difference notes inside #lpn_status, which expire two minutes after they are said
   (Tom, 2026-09-05: "Give it a timer, maybe 2 minutes and maybe fading if that's easy."). The
   transition is the whole of the fade; the span keeps its place in the flow while it runs, or the
   grievance button beside it would jump. `prefers-reduced-motion` gets the disappearance without
   the animation, which is the same outcome a second later. */
.lpn-status-notes {
	transition: opacity 0.8s linear;
}

/* The EPANET run report's own box (ROADMAP Task 570). The text is the engine's, column-aligned
   monospace, so it must NOT wrap -- `overflow-x` inside the box is what keeps the page itself
   from scrolling sideways. The Copy button sits on the title bar, clear of the close X. */
.lpn-rptbox-pre {
	margin: 0;
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: 12px;
	line-height: 1.35;
	white-space: pre;
	overflow-x: auto;
}
.lpn-rptbox-copy {
	position: absolute;
	top: 8px;
	right: 34px;
	font-size: 11px;
	padding: 1px 6px;
}
@media (prefers-reduced-motion: reduce) {
	.lpn-status-notes { transition: none; }
}

/* The suite's one icon set (ROADMAP Task 231). Geometry lives once in lib/Icons.lib.php; this is
   the only place its size and alignment are decided.

   Sized in em, so an icon tracks whatever text it prefixes instead of fighting it at one fixed
   pixel size. stroke:currentColor (set on the element) plus no colour here means an icon is
   automatically right on a light row, a hover row, a disabled row and a dark print stylesheet --
   the property colour emoji could never have. vertical-align sits it on the text's optical
   centre; baseline alignment would hang it low next to a capital letter. */
.ec-icon {
	width: 1.05em;
	height: 1.05em;
	vertical-align: -0.16em;
	flex: none;
}
/* A control whose label is "<icon> Word" gets its gap from here rather than from a space character
   baked into every call site -- a literal space is unremovable and wrong in a narrow toolbar.
   margin-inline-end, not margin-right: in Arabic, Hebrew, Persian, Urdu and Pashto the icon sits
   to the RIGHT of its word and the gap has to follow it there. */
button > .ec-icon, a > .ec-icon, label > .ec-icon { margin-inline-end: 0.4em; }
/* Except inside the lpn menu's fixed-width icon column, where the cell already provides the gap
   and a margin on top of it would push every label off the column it is meant to line up with. */
.lpn-menu-icon > .ec-icon { margin-inline-end: 0; }

/* Hover-tip icons (⚠, ?) that carry a longer explanation in the title attribute */
.ec-tip {
	cursor: help;
	color: steelblue;
	font-size: 0.9em;
}

/* Second line of an input label carrying that field's inline solver control
   (solverControlHtml() in lib/Calculators.lib.php). Deliberately plain: no rule, no
   indent glyph -- the leading button is enough to mark the line as a control, and any
   directional decoration would have to be mirrored for the RTL languages. Line height
   is left alone so the Q input keeps its natural height. */
.ec-solverline {
	display: block;
	margin-top: 2px;
	font-weight: normal;
}

/* Whole-label wrapper: makes the entire label (not just the ? icon) the hover/tap
   target for the tip carried in .ec-tip's title attribute. */
.ec-help {
	cursor: help;
}

/* **A TOOLTIP MAY NEVER TAKE A CLICK.** Bootstrap 5.3 drops its own `pointer-events: none`, so a
   tip that is still up -- and one stays up for as long as its control keeps FOCUS, which a clicked
   toolbar button does -- silently swallows the first click on whatever it is lying over. Found on
   the Settings box: its drag band opens under the toolbar's tip, and the first attempt to drag the
   box did nothing at all. Our tips carry no controls (Bootstrap's `html` option is off), so there
   is nothing inside one to click. */
.tooltip {
	pointer-events: none;
}

/* The welcome line under the page title (template_welcome). Set apart with italics
   rather than the >> ... << that used to be typed into all 27 language strings: those
   are not standard English or typography (Tom, 2026-07-27), they are directional
   decoration that would have to be mirrored for the RTL languages -- the same reason
   .ec-solverline above carries no indent glyph -- and they put presentation inside a
   translated string, where every translator had to copy them by hand. */
.ec-welcome {
	font-style: italic;
}

/* Scripts with no italic tradition, where the browser can only synthesize a slanted
   face and the result reads as a rendering fault rather than as emphasis: Arabic,
   Hebrew, Han, Ethiopic, Devanagari/Bengali, Khmer, Burmese. The line is already its
   own paragraph under the <h1>, so it stays legible with no substitute decoration. */
html[lang="am"] .ec-welcome,
html[lang="ar"] .ec-welcome,
html[lang="bn"] .ec-welcome,
html[lang="fa"] .ec-welcome,
html[lang="he"] .ec-welcome,
html[lang="hi"] .ec-welcome,
html[lang="km"] .ec-welcome,
html[lang="my"] .ec-welcome,
html[lang="ps"] .ec-welcome,
html[lang="ur"] .ec-welcome,
html[lang="zh"] .ec-welcome {
	font-style: normal;
}

/* Looped Pipe Network map editor (prefix lpn_, ROADMAP Task 146). Canvas mechanics ported
   from the validated dev/lpn-spike/canvas-spike.html -- see phase0-acceptance.md. */
/* --lpn-sym is the symbol-size factor (Settings > "Symbol size (relative to text)"), set on the
   SVG element by refreshSymbolSizes() in looped-network.js. Every stroke width below is written as
   its original fixed value times that factor, so the default of 1 renders exactly what shipped
   before the setting existed. Radii and the flow-arrow chevron are geometry ATTRIBUTES, not styles,
   so those are scaled in JS instead. */
/* PLAIN WHITE (Tom, 2026-08-10, very high priority). It was #f7f7f2 -- a faint warm tint that read
   as "paper" on its own and as a dirty smudge next to the white page around it, and that no longer
   matched anything now that a backdrop image can sit under the drawing.

   --lpn-map-bg backs a reservoir/pump map symbol's occlusion patch (.lpn-node-symbol-backdrop/
   .lpn-link-symbol-backdrop below, ROADMAP Task 146.10 follow-up, 2026-08-09), so it must equal
   whatever the canvas itself is painted -- keep it in step with the inline `background` on
   #lpn_canvas in Looped-Network.php. */
/* **THE BARE MAP SAYS `grab`, NOT `default`** (ROADMAP Task 569, Tom 2026-09-06: *"Try a Pan
   cursor."*). It was `default` and that was the whole of a bug he had reported and nobody had
   diagnosed: *"it occasionally flickers from pan/drag cross to default pointer... almost (!)
   reliably at 12px from the point of a node."*

   **THERE WAS NEVER A RING AT 12 px. IT IS A GAP.** dev/browser-pass/specs/cursorflicker.js walks
   outward from a junction one pixel at a time and reads the cursor the browser actually computes:
   the node's own circle ends at ~3.5 px, its LABEL (`.lpn-draglbl`, `cursor: move` -- the
   four-headed arrow he called the pan/drag cross) begins at 9-13 px depending on bearing, and in
   between was bare canvas showing `default`. So a pointer crossing that strip read
   **pointer, default, move**, and his 12 px is simply where the placement pass had put that
   label -- which is why it was there on some bearings and absent on others.

   `grab` rather than `move`: `move` is what a thing you have HOLD of says, and the map is not held
   until the button is down. `grab` is the open hand every map on the web uses for "you may drag
   this", so the strip between a node and its label now reads as more map rather than as nothing.
   The fix is not that the gap closed -- it is that bare map stopped being indistinguishable from
   dead space. */
#lpn_canvas { touch-action: none; cursor: grab; --lpn-sym: 1; --lpn-opacity: 1; --lpn-backdrop-opacity: 1; --lpn-map-bg: #fff; }
/* **AND `grabbing` WHILE THE BUTTON IS DOWN**, which is the other half of the convention and the
   thing that makes the open hand honest: an open hand that never closes is a promise the interface
   does not keep. The class is written by the pan handler that already exists, so nothing new
   decides when a pan is happening. */
#lpn_canvas.lpn-panning, #lpn_canvas.lpn-panning * { cursor: grabbing; }
/* Fading the BACKDROP is the counterpart of fading the symbols below: a busy, dark aerial or plan
   swallows a 0.5-wide pipe and a black flow arrow, and the standard answer everywhere else (AutoCAD
   image fade, QGIS layer transparency) is to knock the reference material back rather than thicken
   the drawing over it -- the drawing then still reads correctly on white and in print. */
.lpn-backdrop { opacity: var(--lpn-backdrop-opacity, 1); }
/* The OpenStreetMap tile layer (ROADMAP Task 145). pointer-events:none because it is scenery: every
   pointer gesture on this page is resolved from the SVG's own coordinates and from the element under
   the cursor, and a tile that could become an event target would sit between the user and every node
   it happens to cover. It shares the backdrop's fade for the same reason the backdrop has one -- a
   dense city map swallows a thin pipe. */
.lpn-basemap { opacity: var(--lpn-backdrop-opacity, 1); pointer-events: none; }
/* Symbol opacity, for laying the network out over a backdrop image: applied to the two whole
   layers that hold symbols, so nodes/pipes/arrows/vertex handles fade together as one drawing
   rather than each fading independently and showing where they overlap. Labels, masks and leaders
   live in their own layers and are deliberately untouched. */
.lpn-symbols { opacity: var(--lpn-opacity, 1); }
/* A junction is a SOLID DOT, one colour, no stroke (Tom, 2026-08-09).
   Two reasons, both from looking at a real drawing. The old two-tone treatment -- #2a6 fill
   inside a heavy #144 ring -- stopped working once the node shrank: "the light green in the
   middle has shrunk to nearly nothing", so it read as a dark blob with a colour fringe rather
   than as a green node. And the ring was most of the size: a 1 unit stroke centred on the edge
   adds 0.5 all round, taking a 1.44 fill out to 2.44 overall, which against a 2.5 unit font's
   ~1.8 cap height is the "about 1.33" too-big he measured by eye. Dropping the stroke and
   sizing the fill alone (JUNCTION_R in looped-network.js) puts the whole dot at one cap height.

   BLACK since 2026-08-14 (Tom: "Black reservoirs, pumps, valves, and nodes, I suppose"), and the
   comment that used to sit here arguing for BLUE-NOT-GREEN has been removed rather than left to
   contradict the rule below -- a stylesheet that explains a colour it no longer uses is worse than
   one that explains nothing.
   The reasoning it made is the reasoning that retired it, which is the satisfying part: "every
   colour we spend on decoration now is one we cannot spend on meaning later". Blue was already the
   restrained choice (EPANET draws everything black; epanet-js draws the normal case in one blue and
   reserves colour for CONDITION). Going the last step to black spends nothing at all on the base
   state, which is what Task 327's colour-by-value view needs in order to mean something -- and it
   answers Tom's own complaint that EPANET makes it "practically an act of Congress to get a pipe to
   show as black while showing values from the model on the map".
   The one existing conditional style, .lpn-node-pending's red ring, still overrides stroke below
   and is unaffected -- which is exactly the point: condition still reads, now against nothing. *//* **A JUNCTION IS A SHADED RING, NOT A SOLID DOT** (Tom, 2026-09-02, stating the convention the
   symbol set follows: *"All the map node symbols are thematic shaded fill. The pump and valve
   symbols are solid fill."*). It was the one node type that did not follow it, and it is the most
   numerous thing on any map. The wash plus its own stroke is the same construction the reservoir
   and tank symbols use, so all three node types now read as one family.

   **BOTH PARTS MOVE ON `currentColor`,** which is why paintNodeColor() writes `color` here rather
   than `fill`: a ring coloured only on its fill keeps a black outline round a coloured middle, and
   a coloured asset takes ONE colour throughout. `color: #000` is the uncoloured state, so clearing
   the inline colour restores it exactly.

   Stroke width scales with `--lpn-sym` like every other world-unit stroke on this map; a fixed
   width would thicken as the drawing zooms out. `.lpn-node-pending`'s red ring still overrides
   stroke below, so the pending-link indicator is unaffected. */
.lpn-node {
	color: var(--lpn-map-ink);
	/* Fallback first, then the opaque tint -- see the backdrop note below for why a translucent
	   wash cannot be used here: a pipe passing under a junction would show through it. */
	fill: var(--lpn-map-bg, #fff);
	fill: color-mix(in srgb, currentColor var(--lpn-shade, 18%), var(--lpn-map-bg, #fff));
	stroke: currentColor; stroke-width: calc(0.14 * var(--lpn-sym, 1));
	cursor: pointer;
}
/* A reservoir now draws as a tank symbol, not this circle (ROADMAP Task 146.10) -- fill:none/
   stroke:none hides the circle itself; `pointer-events: visible` keeps its full disc
   clickable/draggable anyway (data-node, drag, hit-testing all still read THIS element, unchanged),
   same trick used for any UNPAINTED-but-clickable hit target. `.lpn-node-pending`'s ring below still
   overrides stroke, so the pending-link indicator still shows on a reservoir mid-pipe-draw.
   `visible`, not `all`: unpainted is not the same as invisible -- what the user sees here is the
   tank symbol drawn over this disc -- and `all` would also make the disc grabbable if anything ever
   set `visibility: hidden` on it. See the note on `.lpn-draglbl` for the defect that produced. */
.lpn-node-reservoir, .lpn-node-tank { fill: none; stroke: none; pointer-events: visible; }
/* The tank/pump overlay symbols themselves (looped-network.js buildMapIconSvg()) -- purely
   visual, never a click target; the interactive element is always the plain node circle or link
   polyline underneath. `color` is the currentColor the icon paths read for both stroke and the
   reservoir's translucent water-fill path. */
.lpn-node-symbol, .lpn-link-symbol { pointer-events: none; }
/* BLACK since 2026-08-14 (Tom: "Let's turn pumps and reservoirs black now"). This is the same
   argument the blue was chosen under, now being cashed in: colour on this map is the budget we are
   saving for MEANING, and Task 327's colour-by-value view is what will spend it. A reservoir that is
   blue because reservoirs are watery is decoration, and decoration is what makes EPANET "practically
   an act of Congress to get a pipe to show as black while showing values from the model" (Tom,
   2026-08-14). Black base state, colour reserved for what the model says.
   THE TANK GOES WITH IT even though Tom named only pumps and reservoirs: it shares this rule, and a
   lone blue tank among black reservoirs would read as an oversight rather than a distinction --
   the two are told apart by SHAPE, which is the test every symbol here is held to. Easy to revert
   if that was not the intent. The junction dot is deliberately left blue for now; nobody asked. */
/* **BLUE AGAIN SINCE 2026-09-02, WHICH REVERSES THE BLACK ABOVE ON TOM'S OWN WORD** (*"the icons
   are very nice. But they are black"*, and *"the map is black too, but should be thematic blue"*).
   The 2026-08-14 argument is kept above because it is still a good argument and would otherwise be
   re-made: colour is the budget saved for MEANING, and a watery blue reservoir spends it on
   decoration. What changed is that the symbols are now a SET drawn to one convention, and the
   thematic blue is what makes them read as one -- so the hue is carrying the family, not the
   wateriness. Colour-by-value still overrides all of it through currentColor, so the budget is
   spent the moment the model has something to say. One token, so a future change is one edit. */
:root {
	--lpn-map-ink: #1a6faf;
	/* **THE PIPES TAKE THE SAME INK AS THE NODE OUTLINES** (Tom, 2026-09-02: *"Pipes same as node
	   outlines."*, settling the "let's try main/dark" of a minute earlier). ONE ink for the whole
	   drawing, so the network reads as a single object rather than as symbols sitting on a
	   differently-coloured web. A second weight was tried first and is not kept: the pipes are the
	   structure, and making them darker than the things they connect made them read as a separate
	   subject. Colour-by-value still overrides every one of these. */
}
.lpn-node-symbol-reservoir, .lpn-node-symbol-tank { color: var(--lpn-map-ink); }
/* THE SAME BLUE AS THE RESERVOIR, deliberately (Task 248). A tank and a reservoir are both
   water bodies, and colour here is decoration, not meaning -- the two are told apart by SHAPE
   (closed and tall against open and wide, lib/Icons.lib.php), which is what still works in
   greyscale and for a colour-blind reader. Giving the tank its own hue would spend a colour on
   a distinction the shape already carries, and colour is the budget this map is saving for
   CONDITION -- see the note above. */
.lpn-link-symbol-pump { color: var(--lpn-map-ink); }
/* Black with every other symbol (2026-08-14). It was the PIPE's own colour on the argument that a
   valve is a fitting in the line rather than a separate kind of thing -- that argument still holds
   and is now served better, since a black valve on a pipe stroke is a mark ON the line rather than
   a differently-coloured object sitting in it. Told apart from a pump by SHAPE (an angular bowtie
   against a circle), which is the test every symbol here is held to and the only one that survives
   greyscale, print, and a colour-blind reader. */
.lpn-link-symbol-valve { color: var(--lpn-map-ink); }
/* Opaque patch behind a symbol's own linework (prependSymbolBackdrop() in looped-network.js) so a
   pipe running under a reservoir's open tank or a pump's unfilled casing doesn't show through --
   the icon's own paths are untouched, this only paints underneath them. */
/* **THE PATCH IS THE SHADING NOW, AND ONE SHAPE DOES BOTH JOBS** (2026-09-02). It was opaque white,
   which hid the pipe underneath and left the symbol's inside empty. Tom, having looked: *"the nodes
   don't hide the links"* and *"Node outlines alone are colored. The inside is open, and I see
   links."* A translucent wash cannot fix that -- anything you can see the tint through, you can see
   the pipe through. `color-mix()` gives an OPAQUE colour that still reads as an 18% tint of the
   symbol's own hue, so it occludes exactly as the white patch did and carries the thematic shading
   at the same time. The plain `fill` before it is the fallback for a browser without color-mix:
   white and opaque, which is the old behaviour rather than a broken one.
   **NODES ARE SHADED, PUMP AND VALVE ARE SOLID**, which is Tom's own convention (2026-09-02: *"All
   the map node symbols are thematic shaded fill. The pump and valve symbols are solid fill."*). */
.lpn-node-symbol-backdrop, .lpn-link-symbol-backdrop { stroke: none; fill: var(--lpn-map-bg, #fff); }
/* **THE SHADE IS A VARIABLE, AND COLOUR-BY-VALUE TURNS IT OFF** (Tom, 2026-09-04: *"nodes that are
   15% from the source are not dark... Maybe the coloring is working, but it's been artistically
   faded. If so, let's remove that fading."*). He read it exactly right. 18% of a dark ramp colour
   over a white ground is a pale grey whatever the value is, so the low end of a ramp arrived
   looking like the middle of nothing -- and only the top of the ramp, where the ring itself is
   dark, read as coloured at all.
   The wash is the UNCOLOURED state and nothing else. paintNodeColor() writes --lpn-shade: 100% on a
   node it has given a value colour, so the fill becomes that colour outright, and clears the
   property when the colouring is switched off. One inline property beside the one it already
   writes, and the default here is what an uncoloured map keeps. */
.lpn-node-symbol-backdrop { fill: color-mix(in srgb, currentColor var(--lpn-shade, 18%), var(--lpn-map-bg, #fff)); }
.lpn-link-symbol-backdrop { fill: currentColor; }
/* **THE OUTLINE IS A HAIRLINE** (Tom, 2026-09-02: *"I'd like the ring on all node types to be
   thinner, like 1px, with the thematic blue shading."*). The icon paths carry stroke-width="2" as a
   presentation attribute for the toolbar, where the frame is 24px and 2 is right; stretched to a
   map symbol that same 2 reads as a heavy border round a small shape. A stylesheet rule beats a
   presentation attribute, so the map thins it without the toolbar noticing. */
.lpn-node-symbol { stroke-width: 1; }
/* stroke-width was 2 -- too wide relative to a 1.6-2.2 radius node (Tom, 2026-07-30), and it was
   also why the arrow read as absorbed into the pipe rather than a distinct mark. Then 0.5, which
   that correction overshot: a pipe network's PIPES are its primary content, and at 0.5 they were
   drawn lighter than the node outlines (1.0) sitting on top of them. 0.7 is Tom's "a little"
   rather than his "maybe double" -- heavier and easier to see over a busy backdrop, still lighter
   than a node's own outline so the original over-wide problem does not come back. The flow arrow
   is deliberately NOT thickened with it; see .lpn-arrow below. */
/* --lpn-lw, NOT --lpn-sym (Task 331, 2026-08-14). Pipe width is its own setting in screen pixels
   now that text, symbols and links are decoupled: a pipe network's PIPES are its primary content,
   and how heavy they are is a drawing decision in its own right rather than a consequence of how
   big the junction dots happen to be. The three rules below (stroke, closed-link dashes, override
   halo) are the link-weight family and all read --lpn-lw; every other stroke width on the map is
   symbol furniture and stays on --lpn-sym. The multipliers preserve the exact proportions the
   0.7-based versions had, so nothing about the drawing's appearance changed with the decoupling. */
/* BLACK (Tom, 2026-08-14, completing the sweep). The base drawing now spends NO colour at all --
   pipes, nodes, reservoirs, tanks, pumps and valves are one black line-work, and every colour on the
   map from here on means something: condition today (.lpn-node-pending's red, the override amber),
   value tomorrow (Task 327). This is the state Tom named as the thing EPANET makes hard: "a pipe to
   show as black while showing values from the model on the map". */
.lpn-link { stroke: var(--lpn-map-ink); stroke-width: var(--lpn-lw, 0.7); cursor: pointer; }
/* Black with its symbol (2026-08-14) -- the pump's polyline and its circle are one mark. */
.lpn-link-pump { stroke: var(--lpn-map-ink); }
/* No .lpn-link-valve rule: a valve's polyline is a pipe-coloured line by design -- see
   .lpn-link-symbol-valve above. */
/* A CLOSED link (ROADMAP Task 146.07). Dashed, because a closed pipe carries no water and would
   otherwise be indistinguishable from an open one -- an invisible cause for a network that will
   not solve, or that solves to a surprising answer. The dash lengths are in WORLD units and scale
   with --lpn-sym exactly as the stroke width above does, so the dashes keep their proportion to
   the line at every zoom and symbol size rather than dissolving into a dotted line when zoomed
   out. Colour is deliberately unchanged: a closed pipe is still the same pipe, and recolouring it
   would collide with the per-field label colours (lpnFieldColors). */
.lpn-link-closed {
	stroke-dasharray: calc(2.571 * var(--lpn-lw, 0.7)) calc(2 * var(--lpn-lw, 0.7));
}
/* Flow-direction arrow at each link's midpoint (Tom, 2026-07-30, matching EPANET) -- hidden
   until a solve result exists (see updateArrow() in looped-network.js). pointer-events:none so
   it never intercepts a click meant for the link/vertex underneath it.
   An open chevron (unfilled polyline), not a solid triangle: a filled shape in the link's own
   color read as absorbed into the (formerly thick) pipe stroke. Black and open protrudes above
   and below the line instead of blending into it. */
/* NOT multiplied by --lpn-sym, unlike every other stroke width here (Tom, 2026-07-30: "flow arrows
   are too wide… it seemed beautifully narrow before"). The arrow is the one symbol whose SHAPE is
   scaled by an SVG transform (updateArrow() applies scale(symbolFactor()) to the chevron), and an
   SVG transform scales the stroke along with the geometry -- so multiplying here as well squared
   the factor, and the line grew twice as fast as the mark it outlines.
   Left at 0.3 when .lpn-link went to 0.7 (Tom, 2026-07-30): he called this width "beautifully
   narrow", and what makes an arrow vanish into a dark aerial is its pure BLACK against a dark
   background, not its width -- fading the backdrop fixes that without coarsening the mark. */
.lpn-arrow { fill: none; stroke: #000; stroke-width: 0.3; pointer-events: none; }
/* Filled the same color as .lpn-link (Tom, 2026-07-30: vertices should read as "on the pipe",
   not as a third node type) -- previously a hollow white circle with the pump's #a52 stroke,
   which had no relation to the link it belonged to and could be mistaken for a small pump. */
/* **THE VERTEX DOT TAKES THE MAP'S OWN INK** (Tom, 2026-09-02: *"Change vertex color too."*). It
   was black and a near-black stroke while everything around it went blue, which made a bend read as
   a different KIND of thing from the pipe it sits on. It is a mark on the pipe, so it is the pipe's
   colour. */
.lpn-vhandle {
	fill: var(--lpn-map-ink); stroke: var(--lpn-map-ink);
	stroke-width: calc(0.25 * var(--lpn-sym, 1)); cursor: move;
}
/* VERTICES MODE (Task 567): the bends of every pipe become GRIPS -- hollow, and a good deal bigger
   than the dot that merely says a bend is there. EPANET's own grip is a hollow square; a circle is
   what this map already draws, and hollow-plus-large is the half that carries the meaning ("this is
   a thing to take hold of"). The `r` here is an SVG2 geometry property, so a browser that does not
   honour it leaves the grip its ordinary size -- degraded, not broken, because the reach that makes
   a grip grabbable with a finger is nearestVertexNearScreen()'s and not this rule's.
   The whole mode is one class on the canvas, so nothing is created or destroyed to enter or leave
   it, and a bend added while it is on is styled by the same rule with no extra code. */
.lpn-vertexmode .lpn-vhandle {
	r: calc(1.5 * var(--lpn-sym, 1));
	fill: var(--lpn-map-bg, #fff);
	stroke-width: calc(0.4 * var(--lpn-sym, 1));
}
/* Every pipe's bends are legible while the mode is on, not only the selected one: Tom, 2026-09-01,
   *"We could tweak the behavior to enable vertices mode for all pipes if that's efficient."* It is
   -- they are all drawn already. */
.lpn-vertexmode .lpn-link { cursor: crosshair; }
/* ---- Scenarios: the audit halo and the inactive element (ROADMAP Task 184) ----
   AN OUTLINE, NEVER A FILL, and that is the whole reason a link gets a second element rather than
   a restyle: the pipe's own stroke IS the pipe, so recolouring it to mark an override would
   collide with the closed-link dashes, the pump's colour, and every future result-driven styling
   this map spends colour on. A wider line UNDERNEATH composes with all of them.
   Amber rather than red: an override is a deliberate act, not a fault, and red is spent on
   condition here (.lpn-node-pending). Hidden until refreshScenarioMarks() adds .lpn-override,
   which is why every link can carry one at no visual cost. */
.lpn-link-halo { stroke: none; pointer-events: none; }
/* ---- Scale-dependent visibility of GENERATED ANNOTATION (Task 331, 2026-08-14) ----
   Set by applyLabelVisibility() when the visible map is wider than settings.labelMaxWidth.

   THE LINE IS NOT "LABELS", IT IS "THINGS WE GENERATED TO BE READ" -- and the flow arrow proved it
   belongs on this side (Tom, 2026-08-14: "Arrows also should hide at hideable zoom levels"). An
   arrow is a symbol by construction and an annotation by purpose: nobody drew it, it exists to be
   read, and zoomed out it is noise on top of a network you are trying to see the shape of. So:

     HIDDEN  data labels and their leaders (.lpn-datalbl-part), flow arrows
     KEPT    the network itself -- nodes, pipes, pumps, valves, tanks -- and the user's own Text
             labels, which are authored content: they typed the words and chose the spot, and
             vanishing them under a threshold that never mentions Text labels would read as a bug.

   `visibility`, not `display`: a leader and an arrow each have their own show/hide logic and this
   must compose with them rather than overwrite them -- something already hidden for its own reasons
   stays hidden, and something visible is merely made invisible.

   **AND visibility:hidden ONLY DROPS POINTER EVENTS IF THE ELEMENT ASKED IT TO.** This paragraph
   used to end "so nothing suppressed here is still clickable", and that was false for two years'
   worth of SVG: `pointer-events` has two families, and the four names without a `visible` prefix
   (`painted`, `fill`, `stroke`, `all`) hit-test the element REGARDLESS of its visibility. See
   `.lpn-draglbl` below, which wore `all`.

   ONE CLASS, DECLARED AT THE BUILD SITE (Task 334, 2026-08-15). This used to be a list of four
   selectors naming each kind of annotation, and the extrema badge was missed out of it -- Tom
   caught it on screen the same day: "Extrema glyphs forgot to hide when zoomed out." A list in a
   stylesheet has to be remembered by whoever adds the next mark, months later, in another file.
   `annotationEl()` in js/looped-network.js applies this class where the element is created, where
   the author already knows what they are making. Add nothing here; build through that instead.

   The extrema mark itself is no longer even a member: Task 333 made it the number's own
   text-decoration, so it hides, moves and dies with the text. Prefer that where it is available. */
.lpn-labels-hidden .lpn-annotation { visibility: hidden; }
/* A USER'S OWN TEXT LABEL IS NOT ANNOTATION AND IS NOT IN THAT RULE -- it is authored content, and
   it hides on its OWN threshold, scaled by its own size (Task 340): a title block drawn at 3x
   survives to 3x the map width, exactly the way sheet lettering works. Per label, so it cannot be
   a descendant selector; applyLabelVisibility() adds this class to the ones that are over it. */
.lpn-lbl-hidden { visibility: hidden; }
/* ---- THE FIRE FLOW RING (ROADMAP Task 530) ----------------------------------------------------
   The result of the last fire flow sweep, on the junction's own circle. It paints a STROKE while
   the colour ramp paints the FILL, so a run and a pressure colouring can be read at the same time
   and neither has to be turned off to see the other.

   **THE DASH PATTERN CARRIES THE READING AS WELL AS THE COLOUR**, so Passing, Failing and Design
   issue are told apart without telling green from red -- and so a printed page in greyscale still
   says which is which. The three are: solid, dotted, dashed.

   FIRST of the four ring rules that follow, deliberately: override, selection and the pending mark
   are all statements about what you are DOING and each of them overrides this, which is a statement
   about what was calculated. */
.lpn-node.lpn-ff-pass { stroke: #2e7d32; stroke-width: calc(0.5 * var(--lpn-sym, 1)); }
.lpn-node.lpn-ff-fail { stroke: #d32f2f; stroke-width: calc(0.5 * var(--lpn-sym, 1)); stroke-dasharray: 0.6 0.6; }
.lpn-node.lpn-ff-design { stroke: #ef6c00; stroke-width: calc(0.5 * var(--lpn-sym, 1)); stroke-dasharray: 1.6 0.8; }
.lpn-node.lpn-ff-error { stroke: #888; stroke-width: calc(0.5 * var(--lpn-sym, 1)); stroke-dasharray: 0.3 0.9; }
.lpn-link-halo.lpn-override { stroke: #e8a33d; stroke-width: calc(2.857 * var(--lpn-lw, 0.7)); }
/* A node's own circle carries its halo as a ring: it already draws as a filled disc, so a stroke
   around it IS an outline. Placed BEFORE .lpn-node-pending below, so a node that is both
   overridden and mid-pipe-draw shows the pending ring -- that one is about what your next click
   does, and it is the more urgent of the two. */
.lpn-node.lpn-override { stroke: #e8a33d; stroke-width: calc(0.5 * var(--lpn-sym, 1)); }
/* SELECTED: the subject of the next command (ROADMAP Task 415, setSelection() in
   js/looped-network.js). Blue, and it is not spending the map's colour budget on the model: red
   says condition, amber says override, and both are statements about the DOCUMENT, while this one
   is a statement about the VIEW and disappears the moment you click elsewhere. Blue is what every
   editor uses for it, so it needs no legend.
   A pipe wears the mark on its halo -- the same wider line underneath that the override uses -- so
   it composes with the closed-link dashes and the pump's own colour rather than replacing them.
   Both rules sit AFTER their .lpn-override twin (selection is the thing you are acting on now) and
   BEFORE .lpn-node-pending (mid-pipe-draw is about what your next click does, which is more
   urgent still). */
.lpn-link-halo.lpn-selected { stroke: #1976d2; stroke-width: calc(2.857 * var(--lpn-lw, 0.7)); }
.lpn-node.lpn-selected { stroke: #1976d2; stroke-width: calc(0.5 * var(--lpn-sym, 1)); }
.lpn-lbl.lpn-selected { fill: #1976d2; }
/* NOT IN THIS NETWORK: drawn, but out of the model (see isActive() in js/looped-network.js). Faded
   rather than hidden, because the whole point of the inactive/active pair is to see both at once --
   "with the new loop vs. without" wants the proposal visible beside the network it would join. */
.lpn-inactive { opacity: 0.3; }
/* The override marker under a property row, and Base's value beside it. Indented and small so the
   row above stays the property and this reads as a note about it -- there is one of these under
   every editable field once you are inside a scenario, and at full weight they would outnumber and
   outshout the numbers they annotate. */
.lpn-ov-marker { margin-left: 1.5em; font-size: 0.85em; }
.lpn-ov-base { margin-left: 0.6em; color: #666; }
/* Feedback for the first-picked node of a pipe/pump (Tom, 2026-07-30: "otherwise there's no
   indication that anything is working") -- a wide red ring around the existing node fill/stroke,
   cleared by setPendingLinkFrom(null) in looped-network.js on completion or cancel. */
.lpn-node.lpn-node-pending { stroke: #d32f2f; stroke-width: calc(0.5 * var(--lpn-sym, 1)); }
/* Dashed line from the first-picked node to the live pointer, same context (add-pipe/add-pump
   mode) -- shown/hidden and repositioned entirely from JS (rubberBandEl in looped-network.js). */
.lpn-rubberband { stroke: #d32f2f; stroke-width: calc(0.4 * var(--lpn-sym, 1)); stroke-dasharray: 1.2 1; pointer-events: none; fill: none; }
/* No font-size here -- it must be set inline in world units at each text element (see the
   buildNodeEls/buildLabelEls comments in looped-network.js for why a screen-pixel-flavored
   CSS font-size is wrong inside this scaled SVG group). */
/* THE LEGIBILITY BACKING IS A HALO ON THE GLYPHS, NOT A RECT BEHIND THEM (Task 376, the way
   epanetjs does it). `paint-order: stroke fill` paints the stroke first and the fill on top, so a
   white stroke reads as an outline that follows the letters and merges between close characters --
   and it costs one declaration where the rect cost an element per label, a pad constant, a rotation
   kept in step with the text, and a rect left behind whenever a label was hidden.
   **WIDTH IS `em`, NOT --lpn-hair, AND THE DIFFERENCE MATTERS TWICE.** Both are screen-pixel
   constants here -- the font-size on each label is itself a pixel size divided by the scale -- so
   either one answers the defect the old world-unit pad had, where 0.4 world units reached 24px a
   side at working zoom and turned every pipe a label lay along into pale grey. But `em` also stays
   proportional to the LETTERING: a Text label at 3x gets a 3x halo instead of a hairline, and the
   clearance arithmetic in alignedLabelPlacement(), which is written in fractions of the font size,
   stays true at every text size rather than only at the shipped one.
   0.2em is 0.1em of white outside the glyph (~1.1px at the shipped 11px), a little tighter than the
   0.15-of-font-size pad the rect drew. Tighter on purpose: half the stroke falls INSIDE the glyph,
   where the fill paints over it everywhere except in a counter -- the enclosed hole of an e or an
   8 -- and a heavy halo closes those up. `stroke-linejoin: round` keeps a sharp corner from
   throwing a spike. */
.lpn-lbl {
	fill: #333; pointer-events: none; user-select: none;
	paint-order: stroke fill;
	stroke: #fff; stroke-width: 0.2em; stroke-linejoin: round;
}
/* pointer-events:none, found 2026-07-30 while verifying popup placement: labelsLayer draws ABOVE the
   symbol layers, so a leader line crossing a node swallowed the click meant for that node and the
   property popup simply never opened. A leader is not a thing you can click -- a label's own text
   (.lpn-draglbl) is the drag/click target -- so it has no business in hit testing. (The extrema
   badge needed the same rule and no longer exists to need it: Task 333 made the mark a
   text-decoration inside the label's own text.) */
/* ONE SCREEN PIXEL, via --lpn-hair, not a fraction of the symbol size. A leader is a rule pointing
   at something, not a symbol, and scaling it off --lpn-sym made it 0.49px at the shipped symbol
   size and 0.14px if you turned symbols down -- which is why Tom reported "no leaders" while
   looking at them (2026-08-15). A leader that cannot be seen is worse than none: the label it
   belongs to then reads as floating free of the network. */
.lpn-leader { stroke: #000; stroke-width: var(--lpn-hair, 0.1); pointer-events: none; }
/* A LABEL THE USER HAS JUST PLACED SAYS SO, AND THEN STOPS SAYING IT (Tom, 2026-08-15, after
   discovering he had dragged a label without meaning to: "put a timed box or highlight on dragged
   labels for about a minute, maybe fading, maybe dashed, maybe animated so it's obviously
   temporary"). It reads as a notification and never as part of the drawing -- there is no button to
   press and nothing to clean up. */
/* **THE TEXT ITSELF CHANGES COLOUR. NO STROKE, NO BOX, NO NEW OBJECT** (Tom, 2026-08-15, after two
   wrong answers from me: "All we need is something simple like the leader or label changing color
   or blinking temporarily. No new object is needed.")
   He is right, and the simplification deletes the whole bug: a fill has no width, so there is no
   map-unit-versus-screen-pixel question to get wrong -- which is what produced a mark bigger than
   the screen and then a full-page orange starburst, from the same three lines, twice.
   RED, not the orange this started as (Tom: *"Red would be better. It looks orange or brown."*).
   Two quick blinks so it is noticed, then a fade that starts AT ONCE and runs the rest of the way.
   NO `forwards`: when the animation ends the property reverts to whatever the stylesheet says, so
   the label returns to its normal colour on its own and nothing has to clean up after it.

   **THE HOLD IS GONE, AND ITS ABSENCE IS THE POINT** (Tom, 2026-08-16: *"Start the long fade
   immediately. Start appears to be delayed through about half the duration."*). It held full red
   from 16% to 70% -- 24 of the 45 seconds -- and only then began to fade. A mark that does not
   change for half its life reads as a mark that is stuck, so the fade was doing none of the work it
   exists to do: saying "this is temporary" while you look at it.

   The fade segment is LINEAR while the blinks keep the default easing. A per-keyframe
   animation-timing-function governs the segment that STARTS at that keyframe, so this is the one
   place to say it. Linear matters here: eased, most of the colour change happens in the first few
   seconds and the tail is a long crawl through near-black, which is a hold by another name. */
.lpn-just-dragged { animation: lpn-just-dragged 45s ease-out; }
@keyframes lpn-just-dragged {
	0%   { fill: #d40000; }
	4%   { fill: #000; }
	8%   { fill: #d40000; }
	12%  { fill: #000; }
	16%  { fill: #d40000; animation-timing-function: linear; }
	100% { fill: #000; }
}
/* **THE SAME MARK, ON A NODE THAT MOVED** (Tom, 2026-09-01: *"we have no way of knowing when a
   junction moves. Some sort of a fading highlight like the labels have when they are moved would
   help. And this doesn't apply only to the phone, of course."*). ONE mechanism -- the same class,
   written by the same markJustDragged() -- with a second painting, because a dot is not a word.

   NOT `fill`, which is the label version's property and is wrong here twice over: the fill is where
   the colour ramp paints (paintNodeColor() writes it inline), so a 45-second red dot would be a
   pressure reading that is not true, and a reservoir's and a tank's own disc is `fill: none` with
   the symbol drawn over it, so a fill change would show nothing at all. A ring is what every other
   per-node condition on this map already draws -- selection, fire flow, an override, a pending link
   -- and it composes with the ramp instead of overwriting it.

   EVERY DECLARATION IS IN THE KEYFRAMES, NOTHING IN THE RULE, and that is deliberate: nothing ever
   removes the class, so a width or a paint-order declared in the rule would stay on the node for
   ever -- and it would then be the SELECTION ring, months later, that looked wrong. An animated
   property reverts the moment the animation ends. Same reason the label version has no `forwards`.

   `paint-order: stroke fill` is what makes it a HALO instead of a blot: a stroke is centred on the
   circle's edge, so half of a wide one is drawn over the dot. Painting the stroke first and the fill
   over it leaves only the outer half showing, and the node keeps its own colour -- the colour ramp,
   a reservoir's symbol, all of it -- while a red ring stands off it. `.lpn-lbl` uses the same
   property for the same reason, one paragraph up.

   3x, where every other ring on this map is 0.5x. Those say "this is selected", "this failed";
   this one has to be seen by somebody whose finger was ON it a second ago -- Tom, the same day:
   *"the blindness of tapping on a phone is dire."* A junction is drawn at radius 0.9, so a 3-wide
   ring stands 1.5 clear of it: about three times the dot, which is the point. `--lpn-sym` for the
   reason `.lpn-selected` uses it -- this map's user units are metres in one project and DEGREES in
   another, so a bare 3 is a mark bigger than the screen on a lat/lon map.

   Fading to a TRANSPARENT red rather than to black: the ring has to end at nothing, or the last
   frame of the fade would be a black ring that then vanishes when the animation lets go. */
.lpn-node.lpn-just-dragged { animation-name: lpn-just-moved-node; }
@keyframes lpn-just-moved-node {
	0%   { stroke: rgba(212, 0, 0, 1); stroke-width: calc(3 * var(--lpn-sym, 1)); paint-order: stroke fill; }
	4%   { stroke: rgba(212, 0, 0, 0); stroke-width: calc(3 * var(--lpn-sym, 1)); paint-order: stroke fill; }
	8%   { stroke: rgba(212, 0, 0, 1); stroke-width: calc(3 * var(--lpn-sym, 1)); paint-order: stroke fill; }
	12%  { stroke: rgba(212, 0, 0, 0); stroke-width: calc(3 * var(--lpn-sym, 1)); paint-order: stroke fill; }
	16%  { stroke: rgba(212, 0, 0, 1); stroke-width: calc(3 * var(--lpn-sym, 1)); paint-order: stroke fill; animation-timing-function: linear; }
	100% { stroke: rgba(212, 0, 0, 0); stroke-width: calc(3 * var(--lpn-sym, 1)); paint-order: stroke fill; }
}
/* BOTH of them, and the node one by name: `.lpn-node.lpn-just-dragged` above outranks the shorthand
   reset below, so a reader who honours prefers-reduced-motion would still get the blinking ring. */
@media (prefers-reduced-motion: reduce) {
	.lpn-just-dragged { animation: none; }
	.lpn-node.lpn-just-dragged { animation: none; }
}
/* The "you are in the middle of placing the background image" bar (ROADMAP Task 145 follow-up, Tom
   2026-08-16). Fixed to the bottom of the viewport rather than placed near the canvas: while a
   registration sequence runs, every normal interaction on the canvas is suppressed, so the message
   has to be somewhere the eye lands no matter where the user was clicking. High z-index for the same
   reason -- it must outrank the panels the sequence itself puts on screen. */
.lpn-regmode-bar {
	position: fixed; left: 50%; transform: translateX(-50%); bottom: 1em;
	z-index: 3000; display: flex; align-items: center; gap: 0.75em;
	padding: 0.5em 0.9em; border-radius: 4px;
	background: #222; color: #fff; font-size: 0.95em;
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.lpn-regmode-bar button { font: inherit; padding: 0.15em 0.7em; cursor: pointer; }
@media print { .lpn-regmode-bar { display: none; } }
/* **A THING THE USER CANNOT SEE CANNOT BE GRABBED**, and this one keyword is what makes invisibility
   and un-grabbability the same fact instead of two facts that can disagree (Tom, 2026-09-01: *"a
   node label is present but not (yet?) visible, and when I go to pan, the label that I did not see
   drags off the screen."*).

   This rule said `pointer-events: all` and that is the whole defect. SVG's `pointer-events` has two
   families: `visiblePainted`/`visibleFill`/`visibleStroke`/`visible` require `visibility: visible`,
   while `painted`/`fill`/`stroke`/`all` deliberately IGNORE visibility. Four separate mechanisms
   hide a label -- `.lpn-labels-hidden` (thematic colouring and the georeferencing wizard),
   `.lpn-lbl-hidden` (a Text not in this scenario), `setLabelAssemblyHidden()`'s inline style (a node
   label dropped by the collision pass, a link label too short, crowded out or yielded) -- and every
   one of them left a fully grabbable, completely unpainted word lying on the map. MEASURED in
   Chromium: 100% of a hidden label's box still answered `elementsFromPoint()` under `all`, 0% under
   `visible`, and a SHOWN label's grabbable area is identical either way, so nothing is harder to
   pick up than it was.

   `visible` rather than `visiblePainted` because the two are not the same promise: `visiblePainted`
   would also drop the gaps between the glyphs, and a finger is not aimed at the inside of an "8". */
.lpn-draglbl { cursor: move; pointer-events: visible; }
/* Masking is switchable (Task 330, Tom 2026-08-14: "We want to be able to turn off and on
   background masking"). One class on the <svg>, written by applyMaskLabels() from the PROJECT's own
   setting -- masking is a property of how the sheet is meant to be read, not of the browser, so per
   Task 263 it is saved with the project like units and ID prefixes. Since Task 376 "off" is
   `stroke: none` on the text itself; there is no longer an element to hide. */
.lpn-masks-off .lpn-lbl { stroke: none; }
/* Pump curve entry inside the element property popup (Tom, 2026-07-30: "the table needs headings
   for Head and Flow"). Scoped rather than left to the suite's general results-table styling: this
   is a compact input grid inside a floating popup, not a page-width results table, so it must not
   stretch or inherit those borders. Headings left-aligned over their own input column; the row
   label column carries the point number. */
/* **THE TABLE AND ITS ROWS RESET THEIR OWN BORDER, NOT ONLY THEIR CELLS** (Task 553, Tom
   2026-08-28 of the demand editor: "There are two borders on the Demand Categories table, and it
   looks like a mistake."). It is: the suite-wide `table, th, tr, td { border: 1px solid blue }` at
   the top of this file names FOUR elements, and each of these editors reset only two of them. What
   he saw was the table's own blue frame outside the first row's blue rule -- two lines, 1px apart,
   round a compact input grid inside a floating popup.
   Written once for the two popup grids, whose own comments already claimed they "must not inherit
   those borders" and did. **`.lpn-pane-table` is deliberately NOT in this list**: it has the same
   unreset frame, but its blue row rules are the separators of a data grid a person reads every
   day, and nobody has said they are wrong. Removing them is a look, not a defect fix. */
.lpn-curve-table, .lpn-curve-table tr,
.lpn-demand-table, .lpn-demand-table tr { border: 0; }
.lpn-curve-table { border-collapse: collapse; width: auto; margin: 2px 0; }
.lpn-curve-table th, .lpn-curve-table td { padding: 1px 4px 1px 0; text-align: left; font-weight: normal; border: 0; }
.lpn-curve-table thead th { font-weight: bold; }
.lpn-curve-table input { width: 7em; }
/* The demand-categories editor in the junction popup (ROADMAP Task 468). Same shape as the pump
   curve table directly above and for the same reason -- a compact input grid inside a floating
   popup, not a page-width results table -- so it borrows that table's rules and only sets what
   differs: three boxes of its own widths and a remove control.

   THE BOXES ARE NARROW ON PURPOSE. This table has three columns of input plus a button and the
   popup floats over the drawing, which on a phone is 360px wide; at these widths the row fits
   without the popup growing a horizontal scrollbar, and a demand of six significant figures still
   reads. The wrapper scrolls rather than the page if a translation makes a heading wider than its
   column -- a popup that pushes the map sideways is the failure to avoid. */
.lpn-demand-table { border-collapse: collapse; width: auto; margin: 2px 0; display: block; overflow-x: auto; }
.lpn-demand-table th, .lpn-demand-table td { padding: 1px 4px 1px 0; text-align: left; font-weight: normal; border: 0; }
.lpn-demand-table thead th { font-weight: bold; font-size: 0.9em; }
.lpn-demand-table input[type="number"] { width: 5em; }
.lpn-demand-table input[type="text"] { width: 7em; }
.lpn-demand-table select { max-width: 7em; }
/* A glyph, not a button: the same treatment the toolbar's icons get, so a row of data does not
   carry a row of push-button relief. It lights up under the pointer, which is where the "this is a
   control" signal belongs. */
.lpn-demand-del {
	background: none; border: 1px solid transparent; border-radius: 4px;
	font: inherit; line-height: 1; padding: 0 4px; cursor: pointer; color: #a00;
}
.lpn-demand-del:hover { background: #fdd; border-color: #d99; }
.lpn-demand-add { font: inherit; margin: 2px 0; }
/* The Elevation field's DEM control in the node popup (Task 542). A button and, once it has been
   asked, one line saying what the terrain service reported — kept together so the sentence reads as
   belonging to the button rather than to the field below it. */
.lpn-elev-dem { margin: 2px 0 6px; }
.lpn-elev-dem button { font: inherit; }
.lpn-elev-dem .lpn-set-note { margin-top: 2px; }
/* **A TOOLBAR HOLDS ICONS, NOT BUTTONS** (Tom, 2026-08-20: "I have used the word 'Buttons'
   consistently. But in reality, toolbars have icons, not buttons. Change the style to be more like
   a normal toolbar; no 'button' paradigm."). So the chrome goes: no border, no background, no
   push-button relief until the pointer is on one -- which is how every toolbar the user already
   owns behaves, and what the menu bar directly above has always done (.lpn-menubar-item, whose
   hover colours these deliberately match: one strip of chrome, one treatment).

   THE LEADING CHECKMARK IS GONE WITH IT. `content: "\2713 "` in front of an icon shoved the
   drawing sideways and made a pressed tool a different width from an unpressed one; a toolbar says
   "this tool is active" by keeping the icon lit, so a pressed control is a filled well. That is
   not a reversal of Tom's checkmark: he asked for it when these were text buttons, and the mark he
   wanted was "a standard menu 'this is active' mark" -- which is what a menu ROW still gets.

   Bigger, too. An icon with a button around it is read as the button; an icon alone is the target,
   and 1.35em is the size at which a 24-unit drawing survives without one. */
#lpn_toolbar button:not(.lpn-transport-btn) {
	background: none; border: 1px solid transparent; border-radius: 4px;
	font: inherit; color: inherit; padding: 3px 5px; line-height: 1; cursor: pointer;
}
#lpn_toolbar button:not(.lpn-transport-btn):hover { background: #def; border-color: #9bd; }
#lpn_toolbar button:not(.lpn-transport-btn)[aria-pressed="true"] { background: #cfe3f7; border-color: #6aa6d8; }
#lpn_toolbar button:not(.lpn-transport-btn):disabled { opacity: .4; cursor: default; background: none; border-color: transparent; }
#lpn_toolbar button .ec-icon { width: 1.35em; height: 1.35em; vertical-align: -0.25em; }
/* **THE TRANSPORT IS THE ONE EXCEPTION, AND IT KEEPS ITS BUTTONS** (same message: "The Transport is
   the exception, of course; buttons are its paradigm"). Play, step back and step forward are the
   controls of a player, and a player's controls are buttons everywhere from a tape deck to a video
   site. They opt out by class rather than by position, so moving the group cannot silently restyle
   them. Calculate is NOT one of them -- it works the moments out rather than choosing which one you
   are looking at, which is the same reason it is not in the transport's own comment in
   js/lpn-time.js. */
/* **THE STRIP IS A REGION, AND ITS SECTIONS ARE DIVIDED** (Tom, 2026-08-21: "Don't toolbars
   usually have region dividers/borders? Top, sections, bottom as applicable?"). They do, and this
   one did not: a rule under the strip separates it from the tab strip and the map below, and a
   hairline between groups separates the sections from each other.

   This SUPERSEDES 2026-07-30's "a wider gap, not a line ... a divider line was more than needed",
   which was decided when every control on the strip was a word. Words separate themselves; a run
   of bare icons at one spacing does not, and 14 px of air between groups is not legible as a
   boundary when the icons inside a group are 6 px apart.

   Not on .lpn-toolbar-end: it is already pushed to the far edge by an auto margin, so a line
   beside it would mark a gap the eye has already read. */
#lpn_toolbar {
	display: flex; flex-wrap: wrap; align-items: center; row-gap: 4px;
	border-bottom: 1px solid #d6d6d6; padding-bottom: 4px; margin-bottom: 4px;
}
.lpn-toolbar-group { display: inline-flex; gap: 6px; margin-right: 12px; align-items: center; }
.lpn-toolbar-group + .lpn-toolbar-group:not(.lpn-toolbar-end) {
	border-left: 1px solid #d6d6d6; padding-left: 12px;
}
.lpn-transport-btn { font: inherit; cursor: pointer; }
/* The right-hand end of the strip: Find and the bottom-pane toggle (Task 434). Pushed there by an
   auto margin rather than placed at a fixed offset, so it stays at the edge at every window width
   and drops to its own line with the rest when the strip wraps. */
.lpn-toolbar-end { margin-left: auto; margin-right: 0; }
/* ---- Menu bar (ROADMAP Task 211) ----
   Above the toolbar, and holding every command on the page; the toolbar below is the high-use
   subset. Flat text buttons, because a menu bar that looks like a row of push-buttons reads as a
   second toolbar. */
/* **THE MOVABLE BOXES OUTRANK THE CHROME, AND THIS REVERSES A 2026-08-24 RULING ON PURPOSE.**
   That one was Tom's too -- "The menus and toolbars (level 1 menus) need a higher z-index than the
   boxes (Settings, Libraries, ...)" -- and it answered a real defect: a box dragged up the window
   swallowed the strip that had opened it, so the commands sat behind the thing they produced.
   **His ruling of 2026-09-02 is the opposite and supersedes it:** "The boxes that can drag up to the
   top of the page need to cover the menu icons and the project tabs. They need to cover everything...
   these boxes must be sky high, far above everything else, like zindex 1000."

   What changed in between is that a box no longer ARRIVES over the chrome by accident. It is dragged
   there, and it comes to the front when you touch it (raisePanel(), js/looped-network.js), so the box
   covering the menu is always the one you are holding. Do not restore the old order.

   The chrome keeps a z-index of its own so it still outranks the MAP and the two menu pop-ups can sit
   above it (31/32, set inline in Looped-Network.php, because a menu must cover the strip it hangs
   from). Everything movable now starts at 1200: above Bootstrap's fixed navbar (1030) and the toolbar
   (1080), below the registration bar (3000), which is deliberately over everything.
   `position: relative` is what makes z-index apply at all to a static block; it changes no layout. */
#lpn_menubar, #lpn_toolbar, #lpn_tabs { position: relative; z-index: 30; }
/* The floor for every box makePanelDraggable() wires. raisePanel() writes a higher number inline as
   boxes are opened and touched; this is only where the stack starts, and it is what puts an untouched
   box above the chrome. */
/* **THE WHOLE LADDER, IN ONE PLACE, because it was in five and that is how the tips got buried.**
   Everything below 1200 is page furniture; everything at or above it is something the user summoned.

     1200-1799  movable boxes. raisePanel() writes a number inline as they are opened and touched,
                and RENORMALISES at the ceiling -- a counter with no ceiling leaves nothing that can
                be reliably placed above the boxes, which is precisely how the tooltips were lost.
     1850/1851  the menu pop-up and its fly-out. Above the boxes: a menu must be readable even when
                a box has been dragged over the strip it hangs from.
     1860/1870  the modal dialog's backdrop and the dialog. Above the menus; modal means modal.
     1900       tooltips. Above everything summoned, which is Bootstrap's own order (its tooltip
                z-index outranks its modal) and is right: a "?" inside a dialog must be readable.
     3000       the registration bar, which outranks everything while a click sequence is running.

   These four ids carry their number INLINE in Looped-Network.php, not here. That was true of the
   boxes too until 2026-09-02, and it is why `.lpn-dragpanel { z-index: 1200 }` did nothing when it
   was first written: an inline style beats any stylesheet rule, so the boxes were still sitting at
   the 20/22/23 the markup gave them and only raisePanel()'s own inline write moved them. The boxes'
   inline values are gone now and this rule is their floor. */
.lpn-dragpanel { z-index: 1200; }
/* **AND THE TIPS GO ABOVE THE BOXES THEY BELONG TO** (Tom, 2026-09-02: *"Boxes are now in front of
   their own tips. Oops."*). Bootstrap paints a tooltip at its own $zindex-tooltip of 1080, which was
   above every box until the boxes moved to 1200 -- so a "?" inside a box explained itself behind it.
   1900 clears the panel band, which raisePanel() bounds at 1799 for exactly this reason: a counter
   with no ceiling leaves nothing that can be reliably placed on top of it. Still under the
   registration bar (3000). */
.tooltip { z-index: 1900; }
/* **A DIALOG MAY NOT GROW PAST THE WINDOW, AND ITS BUTTONS MAY NEVER LEAVE IT** (Tom, 2026-09-02:
   *"When the import message gets too long, the box runs off the screen and is not recoverable
   without a page reload."*). #lpn_dialog is `position: fixed; top: 20%` with no height bound, so a
   long import report -- and that report grows with how much of a file this page does not model,
   which is exactly the file a user most needs to read about -- pushed its own OK button below the
   viewport. Nothing scrolled: the page itself may not scroll (Task 432), the dialog had no
   overflow, and the button that dismisses it was the part that went missing. A modal you cannot
   dismiss is a lost session, which is why this is a defect and not a polish item.

   The BODY scrolls and the button bar does not, so the way out stays on screen no matter how long
   the message is. 60vh under a top of 20% leaves the buttons and the padding inside the window with
   room to spare, and a short dialog is unaffected because max-height only binds when it is
   exceeded. This is the same rule every other overlay here follows: wide and tall content scrolls
   inside its own container rather than moving the page. */
#lpn_dialog_body { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }
/* **AND THE SUITE NAVBAR'S OWN DROPDOWNS RISE WHILE THEY ARE OPEN** (Tom, 2026-09-02: *"The
   English selector is behind the non-modal (non-hog) boxes."*). Every menu that hangs off that bar -- the language selector among them -- opened behind them. A child cannot climb out
   of its parent's stacking context, so the bar itself has to rise, and it may only do so WHILE a
   menu of its own is open: raising it permanently would put the suite chrome back over the boxes
   and reverse the ruling above. 1852 puts it just over the lpn menu fly-out at 1851, for the same
   reason that one is over the boxes -- a menu must be readable even when a box covers the strip it
   hangs from -- and still under the modal at 1860. `:has()` is what makes "while open" expressible
   in CSS at all; without it this needs a class toggled from JS on every dropdown in the suite.
   **`position: relative` is half the fix and the half that is easy to leave out:** this navbar
   carries no `fixed-top`, so it is statically positioned and a z-index on it does NOTHING on its
   own -- the same trap recorded above for #lpn_menubar. It changes no layout. */
nav.navbar:has(.dropdown-menu.show) { position: relative; z-index: 1852; }
#lpn_menubar { display: flex; gap: 2px; margin-bottom: 4px; }
.lpn-menubar-item { background: none; border: 1px solid transparent; font: inherit; padding: 3px 10px; cursor: pointer; }
.lpn-menubar-item:hover { background: #def; border-color: #9bd; }
/* ---- Project tab strip (ROADMAP Task 211) ----
   Sits directly ON TOP OF THE MAP, below the toolbar (revised 2026-08-04 after seeing the first
   version rendered): with a real menu bar in the chrome above, the strip belongs against the thing
   it names, the way a PDF editor's document tabs and AutoCAD's layout tabs do. Tabs join the content
   below them the way tabs do everywhere: a bottom border on the strip, and the current tab punching
   a hole in it. */
#lpn_tabs {
	display: flex; align-items: flex-end; gap: 2px;
	border-bottom: 1px solid #999; margin-bottom: 6px; padding: 0 2px;
}
/* **overflow-y MUST BE STATED.** A box with `overflow-x: auto` and overflow-y left at `visible`
   computes the y axis to `auto` too (CSS Overflow 3), so the strip grew a vertical scrollbar at
   its right edge for content that is one row tall and can never overflow that way (Tom,
   2026-08-21). `hidden` is the fix; there is nothing above or below a tab to reach. */
.lpn-tabs-scroll { display: flex; align-items: flex-end; gap: 2px; overflow-x: auto; overflow-y: hidden; flex: 1 1 auto; }
.lpn-tab { display: inline-flex; align-items: stretch; background: #ececec; border: 1px solid #999; border-bottom: none; border-radius: 4px 4px 0 0; margin-bottom: -1px; }
/* The current tab is white and open at the bottom -- continuous with the page under it. */
.lpn-tab-current { background: #fff; }
.lpn-tab-current .lpn-tab-name { font-weight: bold; }
.lpn-tab-name, .lpn-tab-caret, .lpn-tab-btn {
	background: none; border: 0; font: inherit; color: inherit; cursor: pointer; padding: 3px 8px; white-space: nowrap;
}
.lpn-tab-caret, .lpn-tab-x { padding: 3px 5px; border-left: 1px solid #ccc; background: none; border-top: 0; border-right: 0; border-bottom: 0; font: inherit; color: inherit; cursor: pointer; }
/* The [X] is on EVERY tab, not just the current one -- that is what the paradigm we are adopting
   does, and the whole return on adopting one is that nobody has to be taught it (Tom, 2026-08-04).
   Muted until hovered so a strip of tabs does not read as a row of close buttons. */
.lpn-tab-x { color: #777; }
.lpn-tab-x:hover { color: #a00; background: #f4d4d4; }
.lpn-tab-btn { border: 1px solid transparent; align-self: center; }
.lpn-tab-name:hover, .lpn-tab-caret:hover, .lpn-tab-btn:hover { background: #ddd; }
/* THE ASTERISK. Full strength means "there are changes this file does not have" -- something to go
   and do. Faded means "this project is in no file at all", a standing condition rather than a task.
   Both genuinely mean unsaved; only the salience differs (Tom, 2026-08-04). If the faded one reads
   as broken, delete the -faint rule and nothing else changes. */
.lpn-tab-star { font-weight: bold; margin-right: 2px; }
.lpn-tab-star-faint { font-weight: normal; opacity: 0.45; }
/* Narrow screens: only the CURRENT tab stays, and the vertical list behind the left-edge button is
   how you reach the others. A map page has no horizontal room to spare on a phone, and a strip that
   wraps to three lines above the map is worse than one button -- but hiding the current tab too
   would take away the one thing the strip exists to say, which is where you are. */
@media (max-width: 640px) {
	.lpn-tabs-scroll .lpn-tab:not(.lpn-tab-current) { display: none; }
}
.lpn-menu-row { display: block; width: 100%; text-align: left; background: none; border: 0; font: inherit; padding: 4px 12px 4px 8px; cursor: pointer; white-space: nowrap; }
.lpn-menu-row:hover:enabled { background: #def; }
.lpn-menu-row:disabled { color: #999; cursor: default; }
/* Group label above the recent-file rows (Task 258). Indented to the icon column so it reads as a
   heading over those rows rather than as a row of its own that failed to get a glyph. */
.lpn-menu-heading { padding: 4px 12px 1px 8px; font-size: 0.8em; color: #666; white-space: nowrap; }
/* "There is more this way" on a row that opens a fly-out (Task 264). Pushed to the trailing edge so
   the arrow lines up down the menu regardless of label length; margin-inline-start, not left,
   because in the five RTL languages the fly-out opens on the other side. */
.lpn-menu-arrow { margin-inline-start: 1.5em; float: inline-end; color: #666; }
/* Reserved icon column for menu rows (Task 231). Fixed width and always present, even when the
   row has no icon, so one iconless row cannot ragged-edge the labels around it. */
.lpn-menu-icon { display: inline-block; width: 1.6em; text-align: center; }
/* The icon inherits the row's colour, so a disabled row greys its icon along with its word with
   no separate rule -- which is the whole reason these are stroked SVG and not colour emoji. */
/* Backdrop registration wizard (Task 146 Phase 2, ported from the spike): during a Scale/Position
   click sequence, regMode already suppresses real interaction in looped-network.js, but the
   per-element cursor:pointer/move rules would still fire on hover, visually implying "this is
   clickable/draggable" when it must be fully ignored. */
#lpn_canvas.regmode, #lpn_canvas.regmode * { cursor: crosshair !important; }
/* Node-mode target step: nodes ARE the valid click target here, so forcing crosshair over them
   removes the one affordance that actually matters. More specific than the rule above (extra
   class), so it wins the !important tie on specificity. */
#lpn_canvas.regmode.regmode-node .lpn-node { cursor: pointer !important; }
/* Popover dismiss control (Tom, 2026-07-31). Was a bottom "Close" button on all four lpn_
   popovers; in the Projects panel it sat directly beneath Open / Rename / Delete -- three buttons
   that all take a PROJECT as their object -- so "Close" read as "close the project" rather than
   "close this panel". A corner X carries no object at all, which is why it is the conventional
   answer, and the ambiguity was not really unique to that one panel.
   The translated word survives as title/aria-label, so the accessible name is still "Close" (and
   still translated) rather than a bare multiplication sign. Sized well past the ~44px touch-target
   guidance -- this page runs on phones, and a bare glyph at its natural type size would be a target
   only a few pixels across. #lpn_popup carries a matching 40px top padding so the button can never
   overlap its first row, which is what a smaller pad plus a full-size hit area would have done.
   ONLY #lpn_popup uses this now (Tom, 2026-08-13). Labels and Settings dropped their X and their
   40px pad: they hang under the toolbar button that opened them, so they are pull-downs, and a
   pull-down with a close button reads as a box that is pretending to be a menu. #lpn_popup is a
   real floating property sheet -- it opens at the point on the map you clicked -- so it keeps its X. */
.lpn-popover-x {
	position: absolute; top: 0; right: 0;
	width: 40px; height: 40px; line-height: 1;
	padding: 0; border: 0; background: transparent;
	font-size: 20px; color: #333; cursor: pointer;
}
.lpn-popover-x:hover, .lpn-popover-x:focus { background: #eee; color: #000; }
/* Popover fit-to-screen (Tom, 2026-07-31, testing on a phone: "everything right of the middle of
   the X is allowed to overflow off the right edge... I would have expected the right edge of the
   popup to align with the right edge of the screen"). The JS clamp in looped-network.js can only
   choose a LEFT edge; when a popover is wider than the viewport, every choice overflows and the
   clamp bottoms out at 4px, spilling the rest off-screen. Capping the width is what makes the clamp
   solvable -- it then lands at left:4px with the right edge 4px in from the far side, which is the
   alignment that was expected.
   Height is capped for the same reason and a worse symptom: a long panel (Settings, or a Projects
   list once a user owns several) taller than a phone screen would be cut off with no way to reach
   the rest -- the same "sized to content, not to screen" trap as the canvas height cap in
   applyMapHeight(). Scrolling lives on an inner body wrapper, not the popover itself, so the
   absolutely-positioned X stays pinned to the corner instead of scrolling away with the content.
   dvh repeats the vh rule for mobile browsers, where vh includes the retracting URL bar and
   overstates the room actually available. */
.lpn-popover { box-sizing: border-box; max-width: calc(100vw - 8px); max-height: calc(100vh - 8px); }
.lpn-popover-body { overflow: auto; max-height: calc(100vh - 56px); }
/* **PROPERTY AND CONDITION SHARE ONE LINE** (Tom, 2026-08-27, of the Find box on a phone). Two
   halves of one phrase -- "Diameter / greater than" -- each narrower than the sentence it belongs
   to, so pairing them costs nothing and buys back a whole line where lines are scarcest.
   `min-width: 0` is the load-bearing half: without it a flex item refuses to shrink below its
   content, and a long condition ("no open path to a source") would push the box wider than the
   phone instead of ellipsizing inside it. */
/* **AND FIND RESIZES** (Tom, 2026-09-04: *"Make lpn Find box resizeable."*). Same shell mechanics
   as the Settings box, for the reason that box's own note gives: one set of box mechanics on this
   page rather than a second. `resize: both` needs a non-visible overflow to be drawn at all, and it
   costs nothing here because the scrolling already lives on `.lpn-popover-body`.

   **THE 22rem MOVED OUT OF THE MARKUP AND BECAME A `width`, WHICH IS THE LOAD-BEARING CHANGE.** It
   was an INLINE `max-width`, and a cap cannot be dragged past. As a definite width it still opens at
   22rem, still refuses to be stretched by a long result -- which is what the cap was really for --
   and now gives way to a drag. resetPanelFill() hands the inline values back on every open, so
   nothing inline may state a size any more or a dragged box would be reset by its own reopening;
   toggleFindPopup() re-applies the remembered one instead, exactly as the Settings box does.

   The floor is a width for the same reason the Settings box's is: this box's own pull-downs stop
   fitting first, and everything below the floor is a sideways scrollbar. */
.lpn-findbox {
	width: min(22rem, calc(100vw - 8px));
	min-width: min(16rem, 94vw);
	flex-direction: column;
	resize: both;
	overflow: hidden;
}
.lpn-findbox .lpn-popover-body { flex: 1 1 auto; min-height: 0; max-height: none; }
.lpn-find-pair { display: flex; gap: 8px; align-items: flex-end; }
.lpn-find-pair > div { flex: 1 1 0; min-width: 0; }
.lpn-find-pair select { max-width: 100%; }
/* The table filter's own line, under the Find button (ROADMAP Task 597): a select saying which
   table and the button that applies the query to it. `flex-end` so the button's baseline sits with
   the select's box rather than with its label, which is the same alignment .lpn-find-pair uses. */
.lpn-find-filter { display: flex; gap: 8px; align-items: flex-end; margin: 6px 0 0; }
.lpn-find-filter > div { flex: 1 1 0; min-width: 0; margin: 0; }
.lpn-find-filter select { max-width: 100%; }
/* The Find panel's query line (ROADMAP Task 540): the three pull-downs written out as one
   sentence, directly above the Find button -- and, since phase 2, the input that writes them back.
   Monospaced, because it is a query and its brackets and quotes have to be countable. Full width of
   the 22rem popover, so a query with an AND in it is readable without scrolling the box. */
.lpn-find-query {
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	font-size: .85em;
	width: 100%;
	box-sizing: border-box;
	margin: 2px 0 2px;
}
/* Tom's own tip, under the input: it says the grammar is expandable. Quiet, because it is a
   standing note rather than an answer to anything the user just did. */
.lpn-find-hint {
	font-size: .8em;
	color: #666;
	margin: 0 0 4px;
}
/* A parse failure. It is the one message on this panel that means NOTHING WILL BE SEARCHED, so it
   is the one that is allowed to be loud. `display` is set from script -- empty means no error. */
.lpn-find-msg {
	font-size: .85em;
	color: #a00;
	margin: 0 0 6px;
	overflow-wrap: break-word;
}
/* Where the three pull-downs used to be, when the typed query says more than they can. Indented and
   quiet: it explains an absence, and the query input under it is what the eye should go to. */
.lpn-find-aside {
	font-size: .85em;
	color: #555;
	margin: 4px 0;
	overflow-wrap: break-word;
}
/* A STANDING BOX IS DRAGGABLE by its chrome -- the padded band around the body, which is the only
   part of it where the event target is the box element itself (see makePanelDraggable()). The
   cursor says so there, and the body puts it back, so a text input never shows a move cursor.
   THE STANDING BOXES ONLY. The menus share .lpn-popover but hang off a button and are dismissed by
   clicking away, so dragging one would be a gesture with nothing to return it. A box that stays put
   until it is closed is the kind that drags.
   `touch-action: none` is what lets a touch drag work at all -- without it the browser claims the
   gesture for scrolling before pointermove ever fires.

   **THE CLASS IS ADDED BY makePanelDraggable() ITSELF** (ROADMAP Task 562), so a box is made
   draggable and touch-draggable in ONE place and a seventh box cannot arrive half-wired. It was an
   id list until then, and the id list had drifted: six boxes were made draggable by script and
   only three were named here, so Find and the two fire-flow boxes could be dragged by a mouse and
   not by a finger -- and this comment named *Find* as one of its three while the selector beside it
   named *Library*. `dev/lpn-spike/panel-touch-harness.js` now asserts the two halves agree. */
.lpn-dragpanel { cursor: move; touch-action: none; }
/* **AND THE BODY TAKES ITS TOUCH GESTURES BACK, WHOLESALE** (2026-09-01). The rule above was
   written for the DRAG BAND -- the padded chrome, which is the only part of the box a drag can
   start on -- but it is written on the box, so it lands on everything inside it as well. The two
   Settings panes were given `touch-action` back by name, and the three other scrolling regions
   inside these boxes were not: the Find results, the fire flow report and its sideways-scrolling
   table, and the property popup's own body. The whole POINT of `.lpn-popover-body` is that it is
   the part that scrolls when there is more box than screen, so declaring it here rather than
   naming panes is the same move `.lpn-dragpanel` itself was: one seam, and a seventh box cannot
   arrive half-wired.
   `cursor: auto` on the same line, so a text input never shows a move cursor. */
.lpn-dragpanel .lpn-popover-body { cursor: auto; touch-action: auto; }
/* The two Settings panes are nested scrollers INSIDE that body, so they say it for themselves --
   a nested scroll region is consulted on its own and is not covered by its ancestor's answer. */
.lpn-setbox-index, .lpn-setbox-content { touch-action: auto; }
/* ...and so are the fire flow report and the sideways scroll its ten-column table lives in. */
.lpn-ff-report, .lpn-ff-tablewrap { touch-action: auto; }
@supports (height: 100dvh) {
	.lpn-popover { max-height: calc(100dvh - 8px); }
	.lpn-popover-body { max-height: calc(100dvh - 56px); }
}

/* Task 244 -- the "Libre Software" mark beside the brand.

   It sits OUTSIDE .navbar-collapse (see lib/Menus.lib.php) so it never hides behind the
   hamburger. Tom, 2026-08-09: it should read "as an extension of the HawsEDC Calculators {}
   Libre Software, almost as one string" -- so it is styled as a quieter continuation of the
   brand, not as a nav destination. Deliberately NOT .nav-link: nav links carry Bootstrap's
   nav padding and hover affordance, which is what made it read as one menu item among many.

   NO SEPARATOR AND NO GAP -- Tom's second browser review, 2026-08-09, marked both on a
   screenshot: "I think A should be removed. And I think that there is no need for gap B.
   Can't they be like one string, separated only by a space?" A was a "|" glyph this rule used
   to draw; B was Bootstrap's own .navbar-brand margin-right: 1rem, which is why zeroing our
   own margin alone would not have closed it. Both are gone, leaving a single space, so the
   name and the claim read as one continuous phrase. They stay distinguishable by colour and
   by the GitHub mark rather than by punctuation.

   margin-inline-start rather than margin-left because 5 of the 27 languages are RTL -- the
   mark must land on the trailing side of the brand in both directions. */
/* The brand and the claim are ONE flex item. .navbar is display:flex with
   justify-content:space-between, so as three siblings (brand, link, hamburger) the free space
   was distributed BETWEEN them and the pair drifted apart as the window narrowed -- exactly
   backwards. As one item there is no gap to grow. align-items keeps the two baselines together
   now that the wrapper, not the navbar, is doing the aligning. */
.ec-brandgroup { display: inline-flex; align-items: baseline; min-width: 0; }
.ec-brandgroup > .navbar-brand { margin-inline-end: 0; }
.ec-nav-libre {
	display: inline-flex;
	align-items: center;
	margin-inline-start: 0.4rem;
	font-size: 0.85rem;
	white-space: nowrap;
	text-decoration: none;
	color: #5a6570;
}
.ec-nav-libre:hover, .ec-nav-libre:focus { color: #1a6faf; text-decoration: underline; }
.ec-nav-libre > .ec-icon { margin-inline-end: 0.3em; }

/* Narrow phones: the brand plus a long translation (bg "Свободен софтуер", id "Perangkat
   Lunak Bebas") plus the hamburger will not co-exist on one line. Drop the word and keep the
   GitHub mark, which is the part that makes the claim checkable; the full wording is still on
   About.php. The link keeps an accessible name because the icon is aria-hidden by design, so
   the text is hidden visually rather than removed from the accessibility tree. */
@media (max-width: 400px) {
	.ec-nav-libre { font-size: 0; }
	/* .ec-icon sizes itself in em, so the font-size: 0 above would collapse the mark to
	   nothing along with the word. Re-assert it in rem, which the parent cannot zero. */
	.ec-nav-libre > .ec-icon { width: 1.15rem; height: 1.15rem; margin-inline-end: 0; }
}

/* NARROW DESKTOP: the band just above the hamburger breakpoint (Tom, 2026-08-10, on a
   Looped-Network screenshot -- "Libre Software and the Hydraulics menu overlap each other, and
   HawsEDC Calculators; the Copy link wrap looks strange").

   Bootstrap's .navbar-expand-lg pins the expanded bar to `flex-wrap: nowrap`, so from 992px up
   to roughly 1150px -- where the brand group, both dropdowns, the Save-this-calculation form and
   the language picker together want more room than there is -- NOTHING yields. Flex shrinks the
   items instead, and because every one of them is white-space:nowrap text, they shrink their
   BOXES without shrinking their content: the words spill out and print on top of each other.
   Below 992px the problem disappears on its own, because the hamburger takes the menus away.

   The fix is to let the row wrap rather than collide. Wrapping is decided before shrinking, so
   whichever piece no longer fits drops to a second line at full width instead of being squeezed
   -- which also cures the Copy link button, whose icon and word were being crushed onto two
   lines by the same squeeze. Costs a little navbar height inside that ~150px band and nothing
   at all outside it. Deliberately NOT solved by truncating the brand: "HawsEDC Calculators" is
   the site's own name, and an ellipsis in it is worse than a taller bar. */
@media (min-width: 992px) {
	.navbar-expand-lg { flex-wrap: wrap; }
	.navbar-expand-lg > .navbar-collapse { flex-wrap: wrap; row-gap: 0.25rem; }
}
/* Belt for the same collision inside the brand group itself: it carries min-width: 0 (so the
   navbar may shrink it), which means its own two nowrap children would otherwise spill out of
   it. Wrapping puts "Libre Software" on a line under the brand instead. */
.ec-brandgroup { flex-wrap: wrap; }
/* The icon and the word are one label, never a two-line stack (Tom's mark C). */
#ec-copy-link-btn { white-space: nowrap; }

/* ============================================================================
   Consent banner and the legal footer links (ROADMAP Task 286)
   ============================================================================
   The one rule here that is a legal constraint rather than a taste call:
   .ec-consent-btn styles BOTH answers identically. Accepting and refusing must
   be equally easy and equally prominent -- same size, same weight, same colour,
   same row. A coloured "Accept" beside a grey "Reject" is the exact dark
   pattern this design exists to avoid, so never give one of them its own rule. */
.ec-consent {
	position: fixed;
	inset-inline: 0;
	bottom: 0;
	z-index: 1080; /* over Bootstrap's fixed navbar (1030) and its dropdowns (1000) */
	background: #fff;
	border-top: 2px solid #1a6faf;
	box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
	padding: 0.75rem 1rem;
	max-height: 70vh;
	overflow-y: auto;
}
/* Bootstrap's reboot gives [hidden] display:none, but the rule above sets no display, so this
   is belt and braces against a future display: flex here silently un-hiding the banner. */
.ec-consent[hidden] { display: none; }
.ec-consent-inner { max-width: 55rem; margin: 0 auto; }
.ec-consent-body { margin: 0 0 0.5rem; }
.ec-consent-current { margin: 0 0 0.5rem; font-style: italic; }
.ec-consent-current:empty { display: none; }
.ec-consent-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin: 0 0 0.5rem;
}
.ec-consent-btn {
	flex: 1 1 12rem; /* equal share of the row, so neither answer is the bigger target */
	padding: 0.5rem 1rem;
	font: inherit;
	color: #fff;
	background: #1a6faf;
	border: 1px solid #1a6faf;
	border-radius: 0.25rem;
	cursor: pointer;
}
.ec-consent-btn:hover, .ec-consent-btn:focus { background: #14588c; border-color: #14588c; }
.ec-consent-links, .ec-legal-links { margin: 0; font-size: 0.9rem; }
.ec-consent-links a, .ec-legal-links a { margin-inline-end: 1rem; }

/* The two English-authoritative legal pages (ROADMAP Task 286). Long prose, not a calculator:
   a reading measure rather than the full window width, and tables that scroll rather than
   squeeze on a phone. */
.ec-legal { max-width: 44rem; }
.ec-legal h2 { font-size: 1.25rem; margin-top: 1.5rem; }
.ec-legal h3 { font-size: 1.05rem; margin-top: 1rem; }
.ec-legal-table { display: block; overflow-x: auto; border-collapse: collapse; margin: 0.5rem 0 1rem; }
.ec-legal-table th, .ec-legal-table td { border: 1px solid #ccc; padding: 0.3rem 0.5rem; text-align: start; vertical-align: top; }

/* ---- The examples gallery (ROADMAP Task 314) --------------------------------------------------
   Sits over the map canvas when there is nothing drawn, and on demand from File > Open example.

   THE WRAPPER (#lpn_empty_hint) STAYS pointer-events:none AND THE PANEL TAKES THEM BACK. That is
   what lets the canvas behind the gallery still be panned in the gaps between the cards, so the
   page reads as a map tool at a glance rather than as a dialog that happens to be full-bleed --
   Tom's "usable middle": the canvas may be VISIBLE behind the gallery, empty, with no project. */
.lpn-examples {
	pointer-events: auto;
	max-width: 68rem;
	margin: 0 auto;
	padding: 1rem 1rem 1.5rem;
	text-align: center;
}
/* The welcome line above the gallery heading (Task 222). Quieter than the heading it sits over:
   the instruction is what the visitor has to act on, the greeting is what they read on the way. */
.lpn-examples-welcome { margin: 0 0 .35em; font-size: 1.05em; }
.lpn-examples-h { margin: 0 0 .15em; font-size: 1.35em; font-weight: 600; }
.lpn-examples-sub { margin: 0 0 .5em; opacity: .75; font-size: .95em; }
.lpn-examples-msg { margin: 3em auto; max-width: 30em; opacity: .7; }
/* ONE ROW WHEN THERE IS ROOM, wrapping down when there is not (Tom, 2026-08-14: "There is no
   guarantee that the examples will all fit... Wrapping degrades gracefully. So it might be better
   to put them all in one row initially").

   auto-fit with a small min is what buys both at once: at full width the six cards share one row,
   and on a narrower window the column count simply drops. The min is deliberately small enough
   that the CURRENT count fits across -- if the shelf grows past what one row can hold, this wraps
   to two rather than shrinking cards to illegibility, which is the right failure. Sub-categories
   and paging (in Tom's picture, not needed yet) drop straight into this without a rewrite. */
.lpn-examples-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
	gap: .7rem;
}
.lpn-example-card {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: .25em;
	padding: .5rem .5rem .6rem;
	border: 1px solid rgba(128, 128, 128, .45);
	border-radius: 6px;
	background: rgba(255, 255, 255, .94);
	color: #222;
	font: inherit;
	text-align: left;
	cursor: pointer;
}
.lpn-example-card:hover, .lpn-example-card:focus-visible {
	border-color: #2a6ebb;
	box-shadow: 0 1px 6px rgba(0, 0, 0, .18);
}
/* **THE DRAWING SITS ON WHITE PAPER, IN BOTH THEMES** (Tom, 2026-08-14: "The examples all have
   very dark backgrounds. It would be better for them to have white backgrounds with a nice
   border"). The thumbnail SVG has no background of its own, so before this it inherited the
   card's -- which the dark-theme rule below turned near-black, and a water network rendered white
   on black does not read as a drawing at all. A plan is a thing on paper; giving the thumbnail its
   own white ground and its own border makes that explicit instead of leaving it at the mercy of
   whatever is behind it. The ink is set here too, so `currentColor` inside the SVG resolves
   against the white rather than against the card. */
.lpn-example-thumb {
	display: block;
	width: 100%;
	height: 6rem;
	object-fit: contain;
	margin-bottom: .15em;
	padding: 2px;
	box-sizing: border-box;
	background: #fff;
	border: 1px solid rgba(128, 128, 128, .4);
	border-radius: 3px;
	color: #17406b;
}
.lpn-example-title { font-weight: 600; font-size: .92em; }
.lpn-example-desc { font-size: .8em; opacity: .8; line-height: 1.3; }
.lpn-example-meta { font-size: .75em; opacity: .6; }
/* Above the grid, not below it -- see renderExamplesGallery(). */
.lpn-examples-blank {
	display: inline-block;
	margin: 0 0 .9em;
	border: 0;
	background: none;
	padding: .3em;
	font: inherit;
	font-size: .9em;
	color: #17406b;
	text-decoration: underline;
	cursor: pointer;
}
@media (prefers-color-scheme: dark) {
	.lpn-example-card { background: rgba(38, 38, 38, .94); color: #eee; }
	.lpn-examples-blank { color: #8ab4e8; }
	/* The thumbnail keeps its white paper and its dark ink -- deliberately NOT themed. */
}
/* Printing a network should print the network, never the shop window. */
@media print { #lpn_empty_hint { display: none !important; } }
/* The gallery's legal row -- this page has no footer, so Privacy/Terms/Cookie settings sit here and
   in the Help menu (ROADMAP Task 314). Same position epanet-js uses in its own arrival panel. */
.lpn-examples-legal { margin: 1.1em 0 0; font-size: .78em; opacity: .7; }
.lpn-examples-legal a { margin: 0 .5em; }

/* ==== BEGIN: colour by value (ROADMAP Task 384) and the thematic map (Task 327) ==============
   Appended as one block on purpose -- nothing above is reformatted, and the whole feature can be
   read (or removed) in one place.

   The element colours themselves are NOT here. They are written as inline styles by
   refreshValueColors() in js/looped-network.js, because a value-driven colour is data, not a rule:
   an inline style is what the stylesheet's own black loses to, and clearing it restores that black
   exactly rather than needing a second rule to undo the first. */

/* TASK 327's THEMATIC MODE HAS NO RULE HERE ANY MORE, and that is the fix for Task 428.
   It used to be `.lpn-thematic .lpn-lbl { display: none }` -- and `.lpn-lbl` is worn by generated
   labels AND by the Text the user placed, so a selector could not tell authored content from
   annotation and the mode silently deleted the user's own notes from view. Tom, 2026-08-18:
   *"Turning off Text on 'no labels' is unexpected."*
   Suppression now goes through applyLabelVisibility() in js/looped-network.js, the ONE seam, which
   hides `.lpn-annotation` -- a membership annotationEl() declares at the build site, where the
   difference is actually known. Do not reinstate a rule here; a second path is the whole defect.
   What survives unchanged: the mode is still reversible because labelSettings is never written,
   and flow arrows still stay, direction not being a number. */

/* The colour key. Its own overlay, created by colorLegendEl() in js/looped-network.js and placed
   by applyColorLegendPosition() from settings.colorLegendPosition -- so top/bottom/left/right are
   deliberately absent here, exactly as they are for #lpn_labels_legend. Two legends need two
   corners; defaulting this one to the opposite corner from the labels legend is why. */
.lpn-color-legend {
	position: absolute;
	font-size: 0.9em;
	line-height: 1.4;
	background: rgba(255, 255, 255, .85);
	padding: 4px 8px;
	pointer-events: none;
}
/* A swatch has to survive being white or very light on white paper, so it carries a hairline
   border of its own -- otherwise the top band of the gray ramp is an invisible row. */
.lpn-color-swatch {
	display: inline-block;
	flex: 0 0 auto;
	width: 1.4em;
	height: 0.85em;
	border: 1px solid rgba(0, 0, 0, .45);
}
/* ==== END: colour by value ================================================================== */

/* The units strip is TWO GROUPS since Task 422: what you enter, and how answers are read. The
   heading is what makes the split legible, so it is quiet but not decorative -- without it the row
   reads as one long list with three quantities inexplicably repeated.

   EACH SELECTOR IS TWO LINES (Task 424, Tom twice): the name above, the control below. Side by
   side, a pair is as wide as its two halves added together and eleven of them made the widest
   thing in the Settings box; stacked, a pair is as wide as its LONGER half, and the strip wraps
   into two or three tidy rows in a box of sensible width. `align-items: end` so a name that wraps
   to two lines still leaves its select on the row's baseline with its neighbours. */
.lpn-units-group { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 4px 10px; }
.lpn-units-head { flex: 0 0 100%; font-weight: 600; opacity: .8; }
.lpn-units-item { display: flex; flex-direction: column; align-items: stretch; }
/* **THIS IS THE TREATMENT THE WHOLE BOX FOLLOWS, NOT AN EXCEPTION TO IT** (Tom, 2026-08-19: "the
   units labels are not uniform with the rest of the box. I like them (and I think they are great
   for saving width), but everything needs to be uniformly designed. Audit all and apply uniform
   styling", and 2026-08-18: "I like the styling of the Input units section"). So this rule is the
   REFERENCE: .lpn-set-row's own name below is written to match it, rather than this being pulled up
   to match the rows. */
.lpn-units-name { font-size: .85em; opacity: .8; line-height: 1.2; }
/* A select is as wide as its widest option unless told otherwise, and one long unit name would
   otherwise set the width of the whole column. It is the same 9rem the setting rows give a control,
   so the strip and the rows are one design. */
.lpn-units-item > select { max-width: 9rem; }

/* ==== The profile view (ROADMAP Task 409) ====================================================
   A chart, not a map: it shares no symbol, scale or colour rule with the drawing. The suite's
   sketch convention applies all the same -- black for what is built, blue for water -- so the
   ground is a black line, the hydraulic grade line is blue, and the pressure between them is the
   same blue at low opacity. No shading, no borders beyond the axis frame, no dimension lines. */
/* The chart FILLS ITS TAB, IN BOTH AXES. Tom, 2026-08-18: "It should use the entire bottom pane,
   and its relative height and width should vary to fill the available height and width."

   **A FIXED viewBox CANNOT DO THAT and it is why the chart sat in a lake of white.** The viewBox
   was a fixed 560x340 and preserveAspectRatio's default ("xMidYMid meet") letterboxes it inside
   whatever box it is given -- so a wide pane got margins left and right and a tall one got them
   above and below, both of them growing as the pane grew. Stretching instead ("none") is worse: it
   distorts the text and the line weights.

   So the viewBox is MEASURED from the host and rebuilt at that aspect ratio (renderProfile()), and
   a ResizeObserver redraws it when the pane is dragged. The CSS's only job is to make the host a
   real box for that measurement to find. */
.lpn-profile-svg { display: block; width: 100%; height: 100%; }
.lpn-profile-frame { fill: none; stroke: #333; stroke-width: 1; }
.lpn-profile-grid { stroke: #ddd; stroke-width: 1; }
.lpn-profile-axis { stroke: #333; stroke-width: 1; }
/* The vertical hairline at each node. Lighter than a gridline: it says WHERE a node is without
   competing with the two lines the drawing is about. */
.lpn-profile-station { stroke: #eee; stroke-width: 1; }
.lpn-profile-ground { fill: none; stroke: #000; stroke-width: 1.5; }
.lpn-profile-hgl { fill: none; stroke: #0645ad; stroke-width: 1.5; }
.lpn-profile-band { fill: #0645ad; fill-opacity: .12; stroke: none; }
.lpn-profile-dot { fill: #000; }
.lpn-profile-tick { font-size: 10px; fill: #333; }
.lpn-profile-nodeid { font-size: 10px; fill: #333; }
.lpn-profile-axistitle { font-size: 11px; fill: #333; }
/* The key, and the waypoint chips. Both are ordinary flow content under the chart. */
.lpn-profile-key { display: flex; flex-wrap: wrap; gap: .1em 1em; font-size: .85em; margin: .2em 0; }
.lpn-profile-key i { display: inline-block; width: 1.4em; height: .6em; margin-right: .35em; vertical-align: middle; }
.lpn-profile-key-ground { border-top: 2px solid #000; }
.lpn-profile-key-hgl { border-top: 2px solid #0645ad; }
.lpn-profile-key-band { background: rgba(6, 69, 173, .12); border: 1px solid rgba(6, 69, 173, .35); }
.lpn-profile-heading { font-weight: bold; margin-bottom: .2em; }

/* ==== The bottom pane (ROADMAP Task 434) =====================================================
   One resizable panel under the map, carrying a tab per thing that is read while the map is
   edited. It is IN NORMAL FLOW below the canvas, which is the whole mechanism: applyMapHeight()
   measures `body.bottom - svg.bottom`, so the pane's height is subtracted from the map's by
   measurement and neither one has to be told about the other.

   `flex: column` with the body carrying the only explicit height -- the grip and the tab strip
   are whatever they need, and JS writes one number in one place. */
.lpn-pane { display: flex; flex-direction: column; border-top: 1px solid #ccc; background: #fff; }
/* The drag handle IS the top edge, which is where a hand aims. Tall enough to hit with a mouse
   and a real amount of pointer slop; the ruled line inside says "grab me" without a glyph. */
.lpn-pane-grip {
	height: 8px; cursor: row-resize; background: #f2f2f2;
	border-bottom: 1px solid #e0e0e0; touch-action: none;
}
.lpn-pane-grip::after {
	content: ""; display: block; width: 3rem; height: 2px; margin: 3px auto 0;
	background: #bbb;
}
.lpn-pane-grip:hover { background: #e8e8e8; }
/* SEVEN TABS, AND THEY WRAP (Task 455). Tom chose wrapping over hiding five behind a type
   selector, so the strip is allowed to become two lines and the X stays pinned to the top-right
   rather than stretching down beside them. The pane's own height accounting reads the chrome by
   MEASUREMENT (paneChromeHeight), so a second line is subtracted from the window like anything
   else -- nothing here assumes one line, and nothing here scrolls sideways. */
.lpn-pane-head { display: flex; align-items: flex-start; gap: 4px; border-bottom: 1px solid #ddd; }
.lpn-pane-strip {
	display: flex; flex-wrap: wrap; align-items: flex-start; gap: 2px 2px;
	flex: 1 1 auto; min-width: 0;
}
/* `display: contents` IS THE WHOLE OF TASK 488. The Print button and the seven tabs must wrap
   as ONE row of items, or the button holds a column and the tabs wrap only within the
   remainder -- which is what Tom saw: "it monopolises a column ... and the table tabs cannot
   wrap past it". Dissolving the tablist's box puts its tabs directly into .lpn-pane-strip's
   flow beside the button, so a second line starts at the pane's left edge and uses its full
   width. The ELEMENT stays, so role="tablist" still owns the tabs and nothing else -- which is
   why the button was not simply appended inside it. */
.lpn-pane-tabs { display: contents; }
/* A TAB, not a push-button: flat, with the selected one joined to the panel below it by having no
   bottom border of its own. Same reasoning as the menu bar's flat items -- a row of raised
   buttons here would read as a second toolbar. */
.lpn-pane-tab {
	background: none; border: 1px solid transparent; border-bottom: 0; font: inherit;
	padding: 3px 12px; cursor: pointer;
}
.lpn-pane-tab:hover { background: #f2f2f2; }
/* The tabs give up their side padding before they give up a line. A narrow window fits more of
   them per row, so the strip wraps to two lines later and to three never. */
@media (max-width: 60rem) { .lpn-pane-tab { padding: 3px 7px; } }
@media (max-width: 40rem) { .lpn-pane-tab { padding: 3px 5px; } }
.lpn-pane-tab[aria-selected="true"] { background: #fff; border-color: #bbb; font-weight: bold; }
.lpn-pane-x {
	flex: 0 0 auto; align-self: flex-start; width: 2.2rem; height: 1.7rem; padding: 0; border: 0;
	background: transparent; font-size: 18px; line-height: 1; color: #333; cursor: pointer;
}
.lpn-pane-x:hover, .lpn-pane-x:focus { background: #eee; color: #000; }
/* The height JS writes lands here. Overflow is the panel's own business, never the page's: the
   root is `overflow: hidden` on this page (Task 432), so anything that does not fit must scroll
   INSIDE its tab. */
.lpn-pane-body { overflow: hidden; }
.lpn-pane-panel { display: none; height: 100%; box-sizing: border-box; padding: 6px 8px; }
.lpn-pane-panel.on { display: flex; }
/* The profile tab: its controls in a fixed left column, the chart taking everything else. A row
   of controls ABOVE the chart would spend the pane's scarcest dimension -- its height -- on
   things that are read once, leaving the drawing the same proof-of-concept size it had as a
   popover. */
.lpn-profile-panel { gap: 10px; align-items: stretch; }
.lpn-profile-controls { flex: 0 0 15rem; overflow: auto; font-size: .9em; }
/* `min-height: 0` matters as much as `min-width`: a flex item's default minimum is its CONTENT,
   and without this the chart could not be measured smaller than the SVG already drawn in it, so a
   pane dragged shorter would leave the chart overflowing rather than reflowing. */
#lpn_profile_chart { flex: 1 1 auto; min-width: 0; min-height: 0; }
@media (max-width: 40rem) {
	.lpn-profile-panel { flex-direction: column; overflow: auto; }
	.lpn-profile-controls { flex: 0 0 auto; }
}
/* A tab whose content is a long list scrolls INSIDE itself -- the page may not scroll (Task 432),
   and a table of 800 junctions is exactly the content that would make it. `block`, not the flex
   row a panel gets by default, and said with enough specificity that it does not depend on which
   rule comes later in this file. */
.lpn-pane-panel.lpn-pane-scroll { overflow: auto; }
.lpn-pane-panel.lpn-pane-scroll.on { display: block; }
/* **NO TOP PADDING ON A SCROLLING PANEL, AND THAT IS THE WHOLE OF THE SLIVER FIX** (Tom,
   2026-08-23: "when you scroll a table, a tiny sliver of the data is visible scrolling above the
   headings. It's a little bit cool and a little bit embarrassing"). A sticky `top: 0` sticks to
   the scrollport's CONTENT edge, so a container padded 6px at the top leaves a 6px band above the
   heading row that the rows scroll through in plain sight -- measured at exactly 6.0px, with a row
   in it. The padding moves into the sticky heading itself below, so the table still opens with air
   above it and there is no band left for anything to show in. */
.lpn-pane-panel.lpn-pane-scroll { padding-top: 0; }
/* The tabular editors. Column width is king: headings stay narrow and cells stay tight. The
   heading row STICKS, because a table you scroll is a table whose headings you lose. */
.lpn-pane-table { border-collapse: collapse; font-size: .9em; }
/* **A FILTERED TABLE SAYS SO ABOVE ITS ROWS** (Task 597). Hidden rows with no visible cause is the
   one way a filter can mislead, so this line is not decoration and never scrolls away with the
   rows: it sits outside the table, in the panel, above the scrollport. The query is printed in the
   same monospace the Find panel writes it in, because it is the same string. */
.lpn-pane-filter {
	display: flex;
	gap: 8px;
	align-items: baseline;
	flex-wrap: wrap;
	font-size: .85em;
	margin: 0 0 6px;
}
.lpn-pane-filter span {
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	overflow-wrap: anywhere;
}
.lpn-pane-filter-clear { font-size: 1em; }
/* **THE FIRST COLUMN IS LEFT AND EVERY OTHER COLUMN IS CENTRED, HEADING AND CELL ALIKE** (Tom,
   2026-08-23: "Right is great justification for numbers. But in tables, center is safest for
   everything but the first column... Make the headings center and we will be back to where we
   started, focusing on the inputs as we should"). Right-aligning the figures was tried and
   overruled: it is right for a column of numerals in isolation and wrong here, where what the
   reader is looking at is the boxes they type in.
   THE CLASS COMES FROM THE COLUMN'S POSITION, not from what it holds, so a table that gains a
   column cannot acquire a second left one. The printed sheet keeps the old names-left,
   figures-right split -- restated in @media print below. */
.lpn-pane-table th, .lpn-pane-table td { padding: 1px 6px 1px 0; text-align: center; border: 0; }
/* start, not left: the first column is the leading one, and in an RTL language that is the
   right-hand edge. Same rule the Labels panel's affix headings follow (Task 435). */
.lpn-pane-table th.lpn-pane-first, .lpn-pane-table td.lpn-pane-first { text-align: start; }
/* z-index, because a sticky cell only outranks the rows by paint order and one positioned thing in
   a row would beat it; padding-top, because the panel's own is gone; and the rule under the
   headings is an INSET SHADOW rather than a border -- under `border-collapse: collapse` the border
   belongs to the table, not to the cell, so it scrolls away from the heading that owns it. */
.lpn-pane-table thead th {
	position: sticky; top: 0; z-index: 2; background: #fff;
	padding-top: 6px; box-shadow: inset 0 -1px 0 #ccc;
}
/* **A CONTROL DOES NOT INHERIT ITS CELL'S ALIGNMENT; IT HAS TO BE TOLD.** Every browser's own
   stylesheet aligns a form control's text for itself -- `start` on an <input>, centre on a
   <button> -- so a <td> that declares an alignment aligns the BOX and not the number inside the
   control standing in it. That is the whole of what Tom saw when the headings moved and the input
   cells did not: one class on both, one of them overruled from inside. `inherit` on every control
   in these tables hands the alignment back to the cell that owns it.
   .lpn-pane-num no longer carries an alignment of its own: on screen the position decides, and
   what "this column is figures" is still worth here is tabular figures. */
.lpn-pane-table th.lpn-pane-num, .lpn-pane-table td.lpn-pane-num { font-variant-numeric: tabular-nums; }
/* **THE BOX WIDTH BELONGS TO THE COLUMN, AND THE COLUMN DECLARES IT** (Tom, 2026-08-23, over three
   rounds of review ending in "round the final widths to nice numbers always": Minor loss 3em,
   Roughness 6em, Length 4.2em, Diameter 3em, and 3.5em for the tank, reservoir and junction
   figures). Roughness is the wide one because a Darcy-Weisbach e is a small number with leading
   zeros. The number lives on the column descriptor in js/looped-network.js and arrives here as
   `--lpn-pane-col-w`; a column that names none keeps the 7em. A hand-kept CSS list keyed by column class was the alternative, and it goes
   silently wrong the day a column key is renamed or a table gains one.
   IT IS A CUSTOM PROPERTY AND NOT AN INLINE WIDTH, because an inline `style="width"` beats every
   stylesheet there is -- including the 360px widths below, which Tom has already approved and
   which this must not move. */
.lpn-pane-table input { width: var(--lpn-pane-col-w, 7em); text-align: inherit; }
/* A heading that sorts and an ID that goes to the map are both flat text buttons: they are the
   cell, not a control sitting in it. */
.lpn-pane-sort, .lpn-pane-goto {
	background: none; border: 0; font: inherit; padding: 0; cursor: pointer; color: inherit;
	text-align: inherit;
}
.lpn-pane-sort { font-weight: bold; }
.lpn-pane-goto { text-decoration: underline; }
.lpn-pane-sort:hover, .lpn-pane-goto:hover { color: #0645ad; }
/* Print, at the LEADING edge of the pane head, ahead of the tab strip (Tom, 2026-08-21: it was
   "poorly discoverable" in the top-right corner, where the eye reads window controls rather than
   an action). flex:0 0 auto keeps it its own width while the tab strip beside it does the
   wrapping, so a strip that wraps to two rows never moves it. It is a small text button rather
   than an icon: it is the only control in this row that DOES something to the content. */
.lpn-pane-print {
	flex: 0 0 auto; align-self: flex-start; margin: 1px 6px 0 0; padding: 2px 8px; font: inherit;
	font-size: .85em; background: #f6f6f6; border: 1px solid #bbb; color: #333; cursor: pointer;
}
.lpn-pane-print:hover, .lpn-pane-print:focus { background: #eee; color: #000; }

/* PRINTING ONE TABLE (Tom, 2026-08-21: "Make a way to print any table").
   The sheet is a STATIC COPY of the active table, built by paneBuildPrintable() and appended
   straight to <body> so that hiding "everything else" is one rule with no ancestor to fight. It
   is display:none on screen and never seen there; it exists only between the button and the
   browser's print dialog, and printPaneTable() takes it away again afterwards.

   The map's own printing is untouched: without .lpn-printing-table on the body nothing below
   applies, so Ctrl+P still prints the drawing exactly as it did. */
#lpn_print_area { display: none; }
@media print {
	body.lpn-printing-table > *:not(#lpn_print_area) { display: none !important; }
	body.lpn-printing-table #lpn_print_area { display: block !important; }
	#lpn_print_area h1 { font-size: 14pt; margin: 0 0 2pt; }
	#lpn_print_area h2 { font-size: 12pt; margin: 0 0 8pt; font-weight: normal; }
	/* The heading row repeats on every sheet -- a page 3 of figures with no headings on it is a
	   page of numbers nobody can read. Not sticky: sticky is a screen idea, and print needs the
	   table-header-group behaviour a <thead> already has. */
	/* **THE SAME WIDTH IT HAS ON SCREEN, WHICH IS ITS CONTENT'S** (Tom, 2026-08-21: "these are
	   very narrow tables, and they are expanded to fit a page"). `width: 100%` stretched a
	   four-column table across the sheet and put inches of white between a label and its
	   number. .lpn-pane-table carries no width at all on screen; the print copy now matches. */
	.lpn-print-table { border-collapse: collapse; font-size: 9pt; }
	/* **THE SHEET KEEPS THE OLD SPLIT: names left, figures right.** The screen went to centred
	   columns because the reader is aiming at the boxes they type in; nobody types on paper, and a
	   printed column of figures reads as one only when it is right-aligned. Restated rather than
	   inherited, because the screen rules above are the same selectors at the same specificity --
	   these win by coming later, and the first column is left in both places either way. */
	.lpn-print-table th, .lpn-print-table td { padding: 1pt 6pt 1pt 0; text-align: left; }
	.lpn-print-table th.lpn-pane-num, .lpn-print-table td.lpn-pane-num {
		text-align: right; font-variant-numeric: tabular-nums;
	}
	.lpn-print-table thead { display: table-header-group; }
	.lpn-print-table thead th { position: static; border-bottom: 1pt solid #000; box-shadow: none; padding-top: 1pt; }
	.lpn-print-table tr { page-break-inside: avoid; }
}

/* ==== The Settings box (ROADMAP Task 441) ====================================================
   One box for everything that belongs to the whole project. A FLOATING BOX, not a pull-down: it
   drags by its chrome and closes by its X, like the property popup and Find.

   TWO PANES: an index that scrolls on its own, and a content pane that scrolls on its own. The
   box carries an explicit height, because two independently-scrolling columns need a container
   with a height to scroll INSIDE -- an auto-height box would simply grow and put the scrollbar on
   the page, which this page does not have (Task 432).

   The size is CSS, not JS: `min()` against the viewport is the clamp, so there is no measure-then-
   write pass to get wrong and the box re-fits a resized window by itself. JS writes only
   `display: flex` / `display: none` and a left/top.

   **LONGER AND LESS THAN HALF AS WIDE** (Tom, 2026-08-18, after using it: "It can be longer and
   narrower; I would say less than half as wide"). 60rem -> 29rem and 38rem -> 46rem. A settings box
   is read down a column, not across a page, and at 60rem the eye had to travel the width of the
   window to get from a name to its control -- while the map it configures was behind it.

   **AND 29rem WAS ONE STEP TOO FAR** (Tom, 2026-08-19: "we may ship the box too narrow"). 34rem.
   Measured: at 29rem the content pane is 316 px, of which the labels lists spend 186 on their four
   fixed columns, leaving ~120 px for a name like "Head loss gradient" -- three lines of it. 34rem
   gives that name ~200 px and costs the map 80 px it is not using. It is still the FIRST-TIME
   width only: setboxLayout remembers whatever the user drags. */
/* **AND IT RESIZES** (Tom, 2026-08-19: "The box is not sizable at all"). `resize: both` needs a
   non-visible overflow to appear at all, and it costs nothing here: the body is a flex column with
   `min-height: 0` and both panes scroll inside themselves, so nothing in the box depends on the
   width or the height it was given.

   **THE FLOOR IS A WIDTH, NOT A HEIGHT.** The index pane is a fixed 6rem, so narrowing the box
   squeezes only the content pane -- and at 25rem the index is already competing for the only width
   there is (see .lpn-setbox-index below). Narrower still and the sideways scrollbar this box was
   narrowed to get rid of comes back. What happens BETWEEN the shipped width and that floor is the
   container query under .lpn-set-row: the setting rows give up their second column and stack.

   **25rem, AND THE NUMBER WAS MEASURED RATHER THAN REASONED.** 24rem looked right on the arithmetic
   -- 384 px less 16 px of padding, 120 px of index and a 10 px gap leaves 238 for a labels list
   whose own floor is 13rem = 208 -- and dev/browser-pass/specs/visibility.js then measured the
   content pane overflowing by 3 px at exactly that width. The same three pixels, and the same
   lesson, as Task 435: the box model is not addable by hand. Height has no such floor -- a short
   box just scrolls -- so 18rem is only enough to keep the search field and a row of content in
   view. */
.lpn-setbox {
	width: min(34rem, 94vw);
	height: min(46rem, 92vh);
	flex-direction: column;
	box-sizing: border-box;
	resize: both;
	overflow: hidden;
	min-width: min(25rem, 94vw);
	min-height: min(18rem, 92vh);
	max-width: 98vw;
	max-height: 96vh;
}
/* **THE TOUCH HALF OF `resize: both`** (Tom, 2026-08-23, on a phone: "I can't figure out how to
   resize Settings on a phone. Is that our fault?" -- it was). The browser's own grabber answers a
   mouse only: mobile Safari paints none at all and Chrome for Android paints one that a finger
   cannot drag. So the property above was, on a phone, a decoration.

   ONLY WHERE A COARSE POINTER EXISTS, so a mouse-only machine never sees this square and keeps the
   native widget it always had -- the desktop is unchanged by construction. `any-pointer`, not
   `pointer`: a touch laptop has a fine PRIMARY pointer and a finger as well, and that hand deserves
   the grabber too.

   28 px, not the 18 px LPN_RESIZE_CORNER reserves for the mouse: that is pointer slop for a cursor.
   Drawn as two diagonal rules, the way every resize corner in every toolkit is drawn, so it reads
   as a grabber without a word of text -- and therefore without a language key. */
.lpn-resize-grip { display: none; }
@media (any-pointer: coarse) {
	.lpn-resize-grip {
		display: block; position: absolute; right: 0; bottom: 0; width: 28px; height: 28px;
		z-index: 2; cursor: se-resize; touch-action: none;
		background:
			linear-gradient(315deg, transparent 0 8px, #999 8px 10px, transparent 10px 13px, #999 13px 15px, transparent 15px);
	}
}
/* The title sits in the 40px drag band, left of the X. It is text, not a control, so it never
   swallows a drag: `e.target` on a pointerdown here is the title, not the box -- hence
   pointer-events:none, which hands the whole band back to the box's own drag handler. */
/* **AND THE SAME THREE HEIGHTS IN dvh** (Tom, 2026-08-22: "The settings box and possibly other
   boxes needs to be height-limited for short screens"). It WAS limited -- to 96vh -- and on a phone
   that is the wrong 100%: vh counts the strip behind the retracting address bar, the same
   measurement error that put the map's own bottom out of reach. So the box opened at 92% of a
   height the reader did not have and its foot sat behind the browser's furniture.

   Keyed off the viewport HEIGHT, not the 640px width: a short window on a laptop is the same
   problem and is not a phone. AFTER the rule it overrides, because @supports adds no specificity
   and an equally specific rule later in the file would simply win. Nothing else is needed -- the
   body is a flex column with `min-height: 0` and both panes scroll inside themselves, so a capped
   box scrolls rather than spilling, and .lpn-libbox borrows this whole shell. */
@supports (height: 100dvh) {
	.lpn-setbox { height: min(46rem, 92dvh); min-height: min(18rem, 92dvh); max-height: 96dvh; }
}
.lpn-setbox-title {
	position: absolute; top: 10px; left: 12px; font-weight: bold; pointer-events: none;
}

/* ---- THE NEW-PROJECT BOX (ROADMAP Task 477) --------------------------------------------------
   Four blocks stacked, each a question. Nothing here sets a width: the box's own max-width in
   Looped-Network.php does that, and the unit strip inside it wraps on its own (.lpn-units-group),
   which is the whole reason those eight selects were stacked name-over-control in Task 424. */
.lpn-new-block { margin: 0 0 10px; padding: 0; border: 0; }
.lpn-new-block > legend { padding: 0; font-size: 1em; font-weight: bold; }
.lpn-new-block > label { display: block; }
/* A radio and its words are one target, and the words are the big half of it. */
.lpn-new-block label > input[type="radio"] { margin-right: .4em; }
.lpn-new-block > input[type="text"] { width: 100%; box-sizing: border-box; padding: 3px 6px; }
/* A disabled place field still has to READ as a question that belongs to the other choice, rather
   than as a broken control: greyed, and the pointer says so. */
.lpn-new-block > input[type="text"]:disabled { background: #f2f2f2; color: #777; cursor: not-allowed; }
.lpn-new-presets { margin: 4px 0 6px; display: flex; gap: 6px; }
/* Create first, and it is the one with weight -- it is what the box is for, and it is what Enter
   presses. NOT a coloured button beside a grey one for a consent-style pair; this is a create/cancel
   pair, where naming the default action is the convention rather than a dark pattern. */
.lpn-new-actions { display: flex; gap: 8px; align-items: center; }
.lpn-new-actions > button:first-child { font-weight: bold; }
.lpn-setbox-body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.lpn-setbox-search { flex: 0 0 auto; padding-bottom: 6px; }
.lpn-setbox-search input { width: 100%; box-sizing: border-box; padding: 3px 6px; }
.lpn-setbox-panes { display: flex; flex: 1 1 auto; min-height: 0; gap: 10px; }
/* The index. Its own scrollbar, because it is as long as the content is and the two do not scroll
   together -- that non-coupling is the whole point of the paradigm.

   **6rem** (Tom, 2026-08-22: the index pane "width can be 80% of current on PC, and desperately
   needs that or narrower on phone") -- 0.8 x the 7.5rem it shipped at. The trade has always been
   the same one and it keeps going the same way: an index row may wrap, and a name on two lines is
   still a name, while a control squeezed to half its width is not still a control. So the 24 px
   goes to the content pane. The phone figure is narrower again and is measured rather than scaled;
   it is in the small-screen block at the foot of this file.

   **AND IT GAVE BACK A TENTH, 6.6rem** (Tom, 2026-08-23, having now used the narrowed box on a PC:
   "Settings.index 1.1"). The 0.8 above was decided sight-unseen; this is the correction after
   looking at it. PC ONLY -- the phone figure he asked about in the same breath went the other way
   and is unchanged at 4.5rem. */
.lpn-setbox-index {
	flex: 0 0 6.6rem; min-width: 0; overflow: auto;
	border-right: 1px solid #ddd; padding-right: 6px; font-size: .9em;
}
.lpn-setbox-content { flex: 1 1 auto; min-width: 0; overflow: auto; font-size: .9em; }
/* **THE DIVIDER BETWEEN THE TWO PANES** (ROADMAP Task 576; Tom, 2026-09-04: *"It might be nice...
   to let the user drag the divider between the settings panes."*). A grab strip, not a line: the
   visible rule stays on .lpn-setbox-index's own border-right, so the drawing does not change and
   only the pointer target is new.

   **THE 10px BETWEEN THE PANES IS UNCHANGED, AND IT IS THE NEGATIVE MARGIN THAT KEEPS IT SO.** A
   third flex child earns a second `gap`, so index-to-content would have gone from 10px to 26px --
   and .lpn-setbox-panes' `gap` is not this rule's to change: the Libraries box borrows the same
   class and has no divider, and the phone rule at the foot of this file subtracts `2px` as its
   share of a 10px gap. `margin: 0 -8px` spends 16 of the 26 back, leaving 2 + 6 + 2 = the same 10,
   for every box and at every width. dev/lpn-spike/small-screen-harness.js measures that arithmetic
   and caught the first attempt, which changed the gap instead.

   `touch-action: none`, the rule dev/lpn-spike/panel-touch-harness.js exists to keep: without it
   the browser claims the drag for scrolling before `pointermove` ever fires, and the divider
   answers a mouse and not a finger.

   **A LINE AND A GRIP, NOT A DOUBLE LINE** (Tom, 2026-09-04, asked for the conventional depiction
   and guessed a double line; researched, and it is not). The double line is the Windows-95 3D
   groove -- a light rule beside a dark one -- and it belongs to a bevelled visual language nothing
   else on this page speaks. What every current design system describes instead is a SEPARATOR LINE
   plus a GRIP mark on the handle, emphasised on hover: Ant Design, Chakra, Telerik's Kendo, Nuxt,
   ServiceNow's Horizon and Balsamiq's own control guidelines all say the same thing in the same
   words. The separator line here is the index's own `border-right`, which was already drawn and is
   shared with the Libraries box; this adds the grip, four dots high, centred on the strip.

   Focusable, so the keyboard can move it (wireSetboxDivider handles the arrows); the focus ring is
   the browser's own, on a 6px-wide element, which is why the grip changes colour on :focus-visible
   as well -- an outline around a hairline is not a visible focus indicator. */
.lpn-setbox-divider {
	flex: 0 0 6px; align-self: stretch; cursor: col-resize; margin: 0 -8px;
	background: transparent; touch-action: none;
	display: flex; align-items: center; justify-content: center;
}
.lpn-setbox-divider::before {
	content: ""; display: block; width: 2px; height: 22px;
	background: repeating-linear-gradient(to bottom, #aaa 0 2px, transparent 2px 5px);
}
/* `:focus`, not `:focus-visible`, and that is the load-bearing half of Tom's 2026-09-04 note. A
   pointer press focuses the divider so Home works from where the hand already is -- but
   `:focus-visible` is defined not to fire for a pointer, so with only that rule the divider would be
   listening and nothing on screen would say so. The grip going blue after a drag IS the message. */
.lpn-setbox-divider:hover::before, .lpn-setbox-divider:focus::before {
	background: repeating-linear-gradient(to bottom, #0645ad 0 2px, transparent 2px 5px);
}
/* **THE LAST SECTION MUST BE ABLE TO REACH THE TOP** (Tom, 2026-09-04: *"We are being bit by the
   bottom of the page when we select the Water Quality bookmark. I believe that the standard answer
   for this is to put a page worth of blank space at the end of the settings."* -- and it is). An
   index row scrolls its heading to the top of this scrollport, which the last heading simply cannot
   do: there is nothing under it to scroll past, so it stops wherever the content ends and the
   reader is left looking at a heading in the middle of the pane, wondering whether the click
   worked. The room is added AFTER the content rather than as padding on it, so nothing inside the
   box moves and no section gains a gap of its own; and it is `min-height` on an empty
   pseudo-element, so it costs nothing to a box that is already taller than its content.

   One viewport of the scrollport itself, not a fixed number of pixels: the box is resizable and on
   a phone it fills the screen, so any constant would be too much on one and too little on the
   other. `100%` of a flex scrollport resolves against its own height, which is exactly the measure
   wanted -- scroll the last heading to the top and the blank space is what sits below it. */
.lpn-setbox-content::after {
	content: ""; display: block; min-height: 100%;
}
/* An index row is flat text, like a menu row: it is a destination, not a command. The SUB rows are
   indented under their section, which is what makes the two levels readable at a glance. */
/* **AND IT WRAPS, WHICH A <button> DOES NOT DO BY ITSELF.** Measured at the 6rem pane: "Node
   symbology" reported a 94 px scrollWidth in a 65 px box -- the row was not wrapping at all, it was
   overflowing, and the index answered with the sideways scrollbar this box was narrowed to get rid
   of. `overflow-wrap: anywhere` covers the other half: "Visualization" is one word and no amount of
   wrapping breaks it, and a name broken mid-word is still a name (CLAUDE.md: mid-word wrap is
   acceptable, column width is king). */
.lpn-setbox-link {
	display: block; width: 100%; text-align: left; background: none; border: 0;
	font: inherit; padding: 2px 4px; cursor: pointer; color: inherit;
	white-space: normal; overflow-wrap: anywhere;
}
.lpn-setbox-link:hover, .lpn-setbox-link:focus { background: #eef4ff; color: #0645ad; }
.lpn-setbox-link.lpn-setbox-link-sec { font-weight: bold; margin-top: 4px; }
.lpn-setbox-link.lpn-setbox-link-sub { padding-left: 16px; }
.lpn-setbox-link[aria-current="true"] { background: #eef4ff; font-weight: bold; }
.lpn-setbox-none { margin: 8px 4px; opacity: .8; }
/* A section heading STICKS to the top of the content pane while its section is being read, so a
   long scroll never leaves the reader wondering which section they are in. */
/* **IN rem, NOT em** (Tom, 2026-08-19: "The main headings are styled SMALLER than the
   sub-headings"). They were: this sits inside .lpn-setbox-content at .9em, so 1.05em came out at
   0.945rem, while every sub-heading below inherits the 1rem that .lpn-set-secbody re-anchors. An
   em-based size for a heading is a size relative to whatever it happens to be nested in, which is
   the wrong thing for a heading to be relative to. */
.lpn-set-head {
	/* **ABOVE THE COLOUR-BAND BOXES, NOT TIED WITH THEM** (Tom, 2026-08-21: the band inputs "cover
	   the main heading above them (Visualization) on scroll"). Those boxes are position:relative
	   z-index:1 so they can straddle their swatch join; at an equal z-index the later element in
	   the DOM wins, and a sticky heading that scrolls UNDER its own section's controls is not
	   sticky. 2 is the whole fix -- the heading is opaque already. */
	position: sticky; top: 0; z-index: 2; background: #fff; margin: 0 0 4px;
	padding: 4px 0; font-size: 1.15rem; font-weight: bold; border-bottom: 1px solid #ccc;
}
.lpn-set-sec { margin-bottom: 14px; }
/* A sub-heading, with NO disclosure triangle: nothing here collapses (Tom, 2026-08-18: "No need
   ever to collapse; just scroll/jump to your section"), and a triangle on something that cannot
   disclose is a lie about the control.

   **WEIGHT 600 AT .8 OPACITY IS THE INPUT UNITS STRIP'S OWN TREATMENT** (.lpn-units-head), which is
   the styling Tom named as the one to converge on: "I like the styling of the Input units section."
   It also puts a whole step between a section heading and a sub-heading, where bold-on-bold left
   the two levels distinguishable only by size. */
.lpn-set-sub { margin-top: 8px; font-size: 1rem; font-weight: 600; opacity: .8; }
.lpn-set-subbody { margin-left: 8px; }
/* The two scope markers, "Project settings" and "Calculator settings". Quieter than a sub-heading
   because they label a SCOPE rather than name a place you can jump to. */
.lpn-set-group { margin-top: 10px; font-weight: 600; opacity: .8; text-transform: none; }
/* **ONE SMALL-TEXT TREATMENT FOR THE WHOLE BOX**, and it is .lpn-units-name's: .85em at .8 opacity.
   The notes under a sub-heading, the ramp acknowledgements, the name beside a unit select and --
   since Tom's 2026-08-19 audit -- the name of every setting row are one size and one opacity. A
   class rather than a style attribute so the next note added inherits the decision instead of
   copying a string; these were three sizes in three places, two of them inline in JS. */
.lpn-set-note, .lpn-rp-credit { font-size: .85em; opacity: .8; }
.lpn-set-note { margin: 2px 0 4px; }
/* A heading INSIDE a sub-heading's body -- the colour band limits, which name the field they belong
   to. Same weight as a sub-heading, not full bold, so it reads as one level further in. */
.lpn-set-minihead { margin-top: 6px; font-weight: 600; opacity: .8; }
/* The Settings box is where the Labels lists live now, and the affix/decimals/priority columns in
   them are the widest thing in it. Anchored at 1rem for the reason Task 435 found: the headings
   and the controls must share a font size or the columns walk. */
#lpn_settings_box .lpn-set-secbody { font-size: 1rem; }

/* ==== The right panel: EMPTY, and kept (ROADMAP Tasks 427, 434 and 441) ======================
   A FIXED OVERLAY over the right edge of the map, placed from the canvas's rect by
   positionRightPane(). It takes nothing away from the measured canvas height (Task 432) and gives
   nothing back, so the map's one measured number stays one number.

   Its width is the user's, dragged on the left edge and clamped in JS; the default is wide enough
   for the Labels rows, which are the tallest and widest thing it holds. */
.lpn-rpane {
	position: fixed; z-index: 6; display: flex; background: #fff;
	border: 1px solid #ccc; box-shadow: -2px 0 6px rgba(0, 0, 0, .12);
}
/* The grip IS the left edge, the mirror of the bottom pane's top edge, and the same 8px of
   pointer slop. */
.lpn-rpane-grip {
	flex: 0 0 8px; cursor: col-resize; background: #f2f2f2;
	border-right: 1px solid #e0e0e0; touch-action: none;
}
.lpn-rpane-grip::after {
	content: ""; display: block; width: 2px; height: 3rem; margin: 1rem auto 0; background: #bbb;
}
.lpn-rpane-grip:hover { background: #e8e8e8; }
.lpn-rpane { flex-direction: row; }
.lpn-rpane-inner { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
.lpn-rpane-head { display: flex; align-items: center; gap: 4px; border-bottom: 1px solid #ddd; }
.lpn-rpane-title { flex: 1 1 auto; font-weight: bold; padding: 3px 8px; }
/* The body scrolls INSIDE the panel: the page may not scroll (Task 432), and the label lists are
   longer than any window this panel will ever be given. */
.lpn-rpane-body { flex: 1 1 auto; overflow: auto; padding: 6px 8px; font-size: .9em; }
/* What the panel says while it holds nothing. */
.lpn-rpane-empty { margin: 0; opacity: .8; }
/* THE ROW SHAPE FOR EVERY SETTING IN THE BOX, AND IT IS ONE GRID OF TWO TRACKS: a name that takes
   the slack and wraps, and a CONTROL COLUMN of one fixed width. `min-width: 0` on the name is what
   lets it wrap instead of forcing the row wider than the pane -- Tom, 2026-08-18: "A few things can
   wrap."

   **THE COLUMN IS WHAT MAKES THE BOX UNIFORM** (Tom, 2026-08-19: "everything needs to be uniformly
   designed. Audit all and apply uniform styling"). As a flex row, each control simply followed its
   name to the right EDGE, so a 13 px checkbox started 131 px right of the 144 px input above it and
   nothing in the box shared an x. Now every control -- checkbox, number, select, ramp picker -- is
   placed at the START of the same column, so one vertical line runs down the whole box and a
   control's WIDTH is free to say what it holds.

   **ALIGNED ON THE FIRST BASELINE, NOT CENTRED** (Tom, 2026-08-19: "The labels checkboxes need to
   stay vertically aligned with their other inputs even if their label wraps below them; checkbox
   even with inputs"). `align-items: center` centred each control on the WHOLE name, so a name that
   wrapped to two lines dropped its checkbox half a line and it no longer sat beside the words it
   answers. A baseline is the first line's, whatever the name does below it.

   It used to be called .lpn-rp-row, after the right pane it was written for, and the colour ramp
   strip borrowed the class for its background: `> :first-child { flex: 1 1 auto }` then made the
   FIRST SWATCH take every spare pixel, which is what Tom saw. A row shape and a strip are not the
   same thing and no longer share a name. */
#lpn_settings_box {
	/* The control column, and the box a number sits in at its left edge. Stated once; every rule
	   below reads them, so there is one place to change either. */
	--lpn-set-ctl: 9rem;
	/* The COLUMN a control sits in, which is not the same length as the control: it also has to
	   hold a box PLUS A BUTTON on one line ("Apply to all" beside a 4-character ID prefix measures
	   190 px in English). A select still stops at --lpn-set-ctl, so the controls themselves are the
	   one width they have always been. */
	--lpn-set-col: 12.5rem;
	/* **AND THE NAME COLUMN HAS A CEILING** (Tom, 2026-08-20, third report: "ID prefixes: The inputs
	   and buttons are still floating right and wrapping paradoxically when the box is wide").
	   MEASURED: with the name track at `1fr` it took every pixel the box was widened by -- 238 px of
	   name at the shipped 34 rem, 974 px at 80 rem -- so the control column, a fixed 9 rem, was
	   dragged from x=1242 to x=1563 and sat hard against the right edge with a thousand px of blank
	   name column to its left, STILL wrapping its button underneath because its own track had never
	   grown at all. That is the paradox: `1fr` is a right-edge rule wearing a left-edge name. With a
	   ceiling the growth stops, the column stays where the reader last saw it, and the spare width
	   goes to nobody -- which is what a settings dialog does. */
	--lpn-set-name: 11.5rem;
	--lpn-set-num: 4.5rem;
	/* A colour band boundary, which is one number in a stack of at most six and needs no room for a
	   name -- Tom, 2026-08-20: "They are too wide. They could be about half as wide." Widened 25%
	   on his 2026-08-21 re-read: half was a shade too tight for a five-digit head in metres. */
	--lpn-set-numsm: 3.125rem;
}
.lpn-set-row {
	display: grid; grid-template-columns: minmax(0, var(--lpn-set-name, 11.5rem)) var(--lpn-set-col, 9rem);
	align-items: baseline; gap: 6px; margin: 3px 0;
}
.lpn-set-row > * { min-width: 0; }
/* A control narrower than its column sits at the column's LEFT edge -- that edge is the one x the
   whole box shares -- and never grows past it. */
.lpn-set-row > :nth-child(2) { justify-self: start; max-width: 100%; }
/* A name in a settings row may break mid-word rather than push the box sideways -- the same rule
   the labels lists carry below, and for the same reason. CLAUDE.md: mid-word wrap is acceptable.

   **AND IT IS DRAWN LIKE A UNIT SELECT'S NAME** (.lpn-units-name: .85em at .8 opacity), which is
   the treatment Tom named twice as the one he likes and the one to converge on. It was the only
   text in the box that named a control and did NOT wear it, which is what he was reading as "not
   uniform". The labels lists' own names are set to match in labelCheckbox(). */
.lpn-set-row > :first-child, .lpn-set-name { overflow-wrap: anywhere; font-size: .85em; opacity: .8; }
/* **AND IT REDUCES GRACEFULLY** (Tom, 2026-08-19: "the width does not reduce gracefully"). Below
   the width that holds a name beside a full control column, the row stacks -- name, then control
   under it. A CONTAINER query, not a media query: what decides this is how far the user dragged
   the box's own edge, and the window may be 2000 px wide while this pane is 250. Written on
   .lpn-set-row rather than on the box, because the box is the container's ANCESTOR and a container
   query cannot restyle the element it is measuring. */
.lpn-setbox-content { container-type: inline-size; container-name: lpnset; }
/* **THERE IS NO MIDDLE BAND ANY MORE** (Tom, 2026-08-20, FOURTH report: "At certain middle widths
   -- yes, paradoxically this doesn't happen at the narrowest, and it doesn't happen at the widest
   -- the buttons wrap under the inputs"). That is exactly what two breakpoints produced, and the
   arithmetic says why: between 17 and 23 rem the row kept TWO columns but shrank the control
   column to --lpn-set-ctl (9rem = 144px), and an ID prefix box plus "Apply to all" measures about
   190px. Below 17rem the row stacked and the group got the whole width, so it fit; above 23rem the
   column was 12.5rem and it fit. Only the band in between was too narrow to hold the pair and too
   wide to stop trying -- the paradox, and the reason three fixes aimed at the wide end all missed.

   So the band is deleted rather than retuned. A row either has room for a name beside a full-width
   control column, or it stacks; there is no width at which it has room for part of one. 24rem is
   the two tracks (11.5 + 12.5) plus their gap, i.e. the first width at which the two-column form
   is honest. A browser without container queries keeps the two columns, which is where this
   shipped. */
@container lpnset (max-width: 24rem) {
	.lpn-set-row { grid-template-columns: minmax(0, 1fr); }
}
.lpn-rp-credit { margin-top: 6px; }
/* **THE ACKNOWLEDGEMENT IS REACHABLE FROM THE PICKER IT IS ABOUT** (Tom, 2026-08-20: "Were we going
   to put a Credits link near the color pickers? Maybe under the Color scheme label?"). A POINTER,
   not a second copy: the licence fixes the wording, so there is still exactly one rendering of it,
   in the footer where nothing has to be read past it, and this line scrolls the reader to it and
   marks it for a moment. */
.lpn-set-creditlink { font-size: .85em; margin: 0 0 4px; }
.lpn-set-flash { outline: 2px solid #0b57d0; outline-offset: 2px; }
/* **A NUMBER BOX IS AS WIDE AS THE NUMBERS IT HOLDS** (Tom, 2026-08-19: "Some inputs are
   gratuitously wide. Comprehensive list: Map appearance, New element prefixes and values, time,
   convergence"). Chrome sizes an `input[type=number]` from its own min/max, so the box shipped a
   144 px convergence tolerance beside a 78 px opacity beside a 62 px flip angle -- three widths for
   three-or-fewer digits, and a box wide enough for a sentence tells the reader to expect one. ONE
   width, here, for the number inputs and for the seven number-shaped TEXT boxes holding `24:00`
   alike. A box that declares its own `size` is left alone -- `size=4` on an ID prefix already says
   "four characters", which is this same rule stated by the author of the control.

   **A CHOICE FILLS THE COLUMN INSTEAD**, because a select is as wide as the words inside it and
   those vary with the language: a select sized to its content gave "Number of colors" 33 px and
   "Legend position" 124 px in the same column. The labels lists' own boxes carry explicit inline
   widths and are unaffected by either rule. The search field is a `search` and is outside the
   section bodies. */
#lpn_settings_box .lpn-set-secbody input[type="number"],
#lpn_settings_box .lpn-set-secbody input[type="text"]:not([size]),
#lpn_settings_box .lpn-set-secbody input.lpn-set-num { width: var(--lpn-set-num); box-sizing: border-box; }
#lpn_settings_box .lpn-set-secbody select { width: 100%; max-width: var(--lpn-set-ctl); box-sizing: border-box; }
/* **AN UNSET HYDRAULICS NUMBER SHOWS NOTHING AT ALL** (Tom, 2026-09-01). This rule used to restyle
   the placeholder -- darker and italic -- to stop an empty box reading as a DISABLED control, which
   was a true diagnosis of a real problem and the wrong fix: it made a number that is not in the
   document look more like one that is. He ruled the number out of the box entirely (*"There's no
   value, and the tip states the default"*), so there is no placeholder on these rows left to style
   and the default is appended to the row's tip instead. See hydNumberRow().

   Deliberately NOT a blanket `input::placeholder` rule for the page: the property popup's
   unitNumberFieldBlank() still shows one, and that one is a different animal -- a live per-node
   value (a reservoir head follows its own elevation), not a constant a tip could ever state. */
/* A control that is a BOX PLUS A BUTTON -- the ID prefixes' "Apply to all", the label view width's
   "Use current view" -- stays inside the column and wraps its button under its box, rather than
   growing left out of the column and taking that row's one x with it. */
.lpn-set-ctlgroup { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px; width: 100%; }
/* **HOW A VALUE IS ALIGNED INSIDE ITS CONTROL, once, for the whole box** (Tom, 2026-08-19: "Some
   inputs are right justified. Others are not. Standardize with an eye for design").

   THE RULE, and the next control follows it without asking:
     * A NUMBER is right-aligned, so digits line up down the column and 7 and 100 can be compared at
       a glance. That is `input[type="number"]`, plus `.lpn-set-num` for the number-shaped TEXT
       boxes -- the seven time fields hold `24:00`, which is a number to every reader and a string
       only to the parser.
     * WORDS ARE NOT. Text boxes (the Before/After affixes), selects, checkboxes and the ramp
       picker keep their natural start alignment, which is also what makes them read correctly in
       an RTL language.
     * ONE EXCEPTION, and it is about the widget rather than the value: a box that draws its own
       spinner arrows (`.ec-spin` -- the Decimals and Rank columns) has no free right edge, so its
       single digit is CENTRED under its centred heading instead. */
#lpn_settings_box .lpn-set-secbody input[type="number"],
#lpn_settings_box .lpn-set-secbody input.lpn-set-num { text-align: right; }
#lpn_settings_box .lpn-set-secbody input.ec-spin { text-align: center; }
/* The ramp strip: a full-width band whose boxes are placed by js/lpn-ramps.js's swatchBoxes(), each
   at x = index * width/n. `position: relative` is the frame those percentages are measured in, and
   border-box keeps each swatch's own hairline border inside its share instead of adding to it. */
.lpn-ramp-strip { position: relative; height: 0.9rem; margin: 4px 0; }
.lpn-ramp-strip .lpn-color-swatch { top: 0; height: 100%; width: auto; box-sizing: border-box; }
/* THE RAMP PICKER: a button showing one bar, and a popup that is a scrolling column of bars.
   Not a <select>, because a <select> cannot hold a picture -- Tom, 2026-08-18: "the dropdown has
   names instead of colors". `position: relative` here is what the popup is placed against, and
   it fills the row's control column, like a select, so the picker lines up with every control above
   and below it. */
/* A PICTURE HAS NO BASELINE, so it opts out of the row's baseline alignment and sits at the top of
   the row instead -- aligned on its first line like everything else, without a text baseline to be
   aligned by. */
.lpn-ramp-picker { position: relative; width: 100%; align-self: start; }
/* ...and "like a select" is now a rule rather than a coincidence: the control column is wider than
   a control since 2026-08-20, so the bar stops where a select stops instead of filling the column. */
#lpn_settings_box .lpn-ramp-picker { max-width: var(--lpn-set-ctl); }
.lpn-ramp-btn {
	display: block; width: 100%; padding: 2px 3px; background: #fff;
	border: 1px solid #767676; border-radius: 3px; cursor: pointer;
}
.lpn-ramp-btn .lpn-ramp-strip { margin: 0; }
/* The list is taller than the panel and scrolls inside itself, for the same reason every other
   overlay here does: the page may not scroll (Task 432). 41 ramps is a column, not a menu. */
.lpn-ramp-pop {
	position: absolute; z-index: 20; top: 100%; right: 0; width: 15rem; max-height: 60vh;
	overflow-y: auto; padding: 4px 6px; background: #fff; border: 1px solid #999;
	box-shadow: 0 4px 10px rgba(0, 0, 0, .2);
}
/* A family heading and its abbreviated example. The example is quieter because it is a hint about
   the DATA, not a name to choose from -- the rows below carry no names at all. */
.lpn-ramp-fam { font-weight: bold; font-size: .85em; margin: 8px 0 2px; }
.lpn-ramp-fam:first-child { margin-top: 0; }
.lpn-ramp-fam-eg { font-weight: normal; opacity: .75; }
.lpn-ramp-opt { padding: 2px 3px; cursor: pointer; border: 1px solid transparent; }
.lpn-ramp-opt:hover, .lpn-ramp-opt:focus { border-color: #0b57d0; background: #eef3fd; outline: none; }
.lpn-ramp-opt[aria-selected="true"] { border-color: #333; background: #f0f0f0; }
/* A break the user typed that this file will not accept: marked where it is, so no message has to
   carry a box number into 27 languages. The map is left exactly as it was, which the message says. */
.lpn-bad { outline: 2px solid #b00020; }
.lpn-color-msg { color: #b00020; font-size: .85em; margin: 2px 0; }
/* **THE BAND BOUNDARIES ARE A LEGEND, NOT A ROW OF BOXES** (Tom, 2026-08-20: "Color band boundaries
   inputs: They are too wide. They could be about half as wide. But the real problem is that they are
   not fitting in the width of the box, and we should make them vertical, one row for each color").

   ONE ROW PER COLOUR, and the swatches touch: with no row gap the first column is a continuous ramp
   in the same ascending order the numbers are typed in, lowest band at the top. A boundary is a LINE
   BETWEEN TWO BANDS -- there is one fewer of them than there are colours -- so its box is pushed
   half its own height down and STRADDLES that line, touching both bands it separates. Nothing has to
   be counted out, and the last band's row correctly has no box: nothing bounds it from below.
   `translateY` rather than a negative margin, so the rhythm the swatches are drawn on is exactly the
   rhythm the boxes are placed against. */
.lpn-color-breaks {
	display: grid; grid-template-columns: 1.4em var(--lpn-set-numsm, 2.5rem);
	column-gap: 6px; row-gap: 0; margin: 4px 0 10px; width: max-content;
}
/* Each band draws its own bottom hairline and inherits the one above it, so a stack of six is five
   single lines rather than five doubled ones. */
.lpn-color-breaks > .lpn-color-swatch {
	width: auto; height: auto; align-self: stretch; min-height: 2.1em; border-top-width: 0;
}
.lpn-color-breaks > .lpn-color-swatch:first-child { border-top-width: 1px; }
.lpn-color-breaks > input {
	align-self: end; transform: translateY(50%); position: relative; z-index: 1;
}
/* Wins over the box's own one-width-for-every-number rule ON PURPOSE, and only here: these boxes are
   a legend rather than a column of settings, and Tom asked for half. */
#lpn_settings_box .lpn-set-secbody .lpn-color-breaks > input.lpn-set-num { width: var(--lpn-set-numsm); }
/* The colour key is a WAY IN to the colour controls (Task 427), so it has to take a click -- the
   overlay stack is pointer-events:none by default and this one cell opts back in. */
.lpn-color-legend { pointer-events: auto; cursor: pointer; }

/* ==== THE SATELLITE TEASER (ROADMAP Task 452) ================================================
   Tom, 2026-08-22: *"Should there be a little 'satellite' teaser tile/button in the corner of the
   map like at Google Maps?"* It is a cell of #lpn_map_footer rather than a corner of its own -- see
   the markup comment in Looped-Network.php for why the corners were all spoken for.

   **THE PICTURE IS DRAWN HERE AND FETCHES NOTHING.** A real tile behind it would be a third-party
   request made before the user asked for one, which is the thing the whole basemap is opt-in to
   avoid. So is a hosted image of the earth: the drawing below is an inline SVG in this stylesheet,
   and nothing about it leaves the page.

   IT IS THE WORLD (Tom, 2026-08-23: *"For the tile/button, we should use something that looks like
   the world. Google uses an actual thumbnail of a tile nearby or something like that... we could use
   something like [Blue Marble]"*). What it replaced was two crossed gradients meant to read as
   fields and a road, and at 40px they read as a texture rather than as anywhere. A coastline cannot
   be faithful at 40px and would turn to mud; what carries is four masses in the right PLACES --
   North America upper left, South America below it, Eurasia across the top with Africa hanging from
   its middle, Australia lower right. Checked at 20 and 40px with
   `dev/scripts/icon_ascii_preview.php --geom=`, which is the only rasterizer this project has, and
   then looked at. The rounded corners and the 1px border are the other half of what epanet-js's own
   switcher does well: at 40px they are what separates a tile from the map behind it.

   THE PRESSED STATE IS THE OTHER SOURCE. When satellite images are already showing, the tile shows
   a pale street map, because a toggle's picture is what you get by pressing it --
   Google's own behaviour, and the one Tom is citing. `aria-pressed` carries the same fact for a
   reader who gets no picture at all. Neither picture asks the page theme anything: an ocean this
   dark and a land this green are both legible against a light page and a dark one, and the button
   paints its own ground either way. */
.lpn-basemap-teaser {
	pointer-events: auto;
	width: 40px; height: 40px; flex: 0 0 auto;
	padding: 0; cursor: pointer;
	border: 1px solid #666; border-radius: 4px;
	box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
	background-color: #10304f;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%2310304f'/%3E%3Cg fill='%233d8b3d'%3E%3Cpath d='M1.8 3 L9.6 2.6 L8.8 5.8 L7.2 6.6 L6.4 9.2 L5 8.4 L3.4 6 L1.8 4.4 Z'/%3E%3Cpath d='M7 10 L10.6 9.4 L10.2 12.8 L8.8 15.6 L8 19 L7 15 L6.4 11.8 Z'/%3E%3Cpath d='M10.6 3.2 L22.6 2.6 L22.8 7.4 L18 8.8 L16.2 8.4 L16 10.6 L15 14 L13.6 16.8 L12.4 13 L12.2 9.2 L10.2 6.2 Z'/%3E%3Cpath d='M18.6 14.2 L22.6 13.6 L22.8 17.4 L19.4 18 Z'/%3E%3C/g%3E%3C/svg%3E");
	background-size: 100% 100%;
	background-repeat: no-repeat;
}
.lpn-basemap-teaser:hover, .lpn-basemap-teaser:focus { border-color: #0b57d0; }

/* ==== The one-tap grievance link (ROADMAP Task 207, Rung 0) =================================
   QUIET IS THE SPECIFICATION, not a taste. This page is a full-window drawing surface, and a
   standing affordance that competes with the drawing is a worse defect than no affordance at
   all -- so it reads as one more small readout in the bottom strip until it is pointed at, and
   only then does it look like a control.

   It is a <button> and never an <a>: nothing navigates, one press posts one row and the label
   turns into the thank-you in place. Two homes, one class -- the bottom strip and the solver's
   amber diagnostic box -- so the background is transparent and it takes whatever it is sitting
   on rather than carrying a colour that is right in one place and wrong in the other.

   pointer-events:auto because both of its homes are inert overlays over the map. */
.lpn-wrong-btn {
	pointer-events: auto;
	background: none;
	border: 1px solid transparent;
	border-radius: 3px;
	padding: 0 4px;
	font: inherit;
	font-size: 11px;
	color: #555;
	cursor: pointer;
}
.lpn-wrong-btn:hover, .lpn-wrong-btn:focus { border-color: #bbb; color: #222; }
/* Once tapped it is no longer a control, and it must not keep looking like one: the label is the
   thank-you, so a hover border on it would invite a second press that posts nothing. */
.lpn-wrong-btn[disabled] { color: #555; cursor: default; border-color: transparent; opacity: 1; }
/* THE CORNER PEEL, and it is the one part of epanet-js's own switcher worth taking (Tom,
   2026-08-23, pasting two screenshots of it: *"It's acceptable now. But see what I see at
   epanetjs"*). A tile that shows only the destination says what you get and not what you leave;
   the peeled corner says both at once, and at 40px the wedge is an 18px triangle -- measured, not
   guessed. It goes on the STREET-MAP picture, which is the one showing while satellite is on, so
   the imagery is what shows through the peel.

   WHAT WAS DELIBERATELY NOT TAKEN: their tile carries the mapbox wordmark baked into its bottom-
   left corner. Naming a control after its provider is promotion, and the attribution it discharges
   for them is already discharged here by #lpn_basemap_credit, which appears whenever a tile does. */
.lpn-basemap-teaser.lpn-basemap-teaser-on {
	background-color: #eae7e0;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%23eae7e0'/%3E%3Cg stroke='%23c3bdb1' stroke-width='0.9' fill='none'%3E%3Cpath d='M0 6.5 L24 6.5'/%3E%3Cpath d='M0 13.5 L24 13.5'/%3E%3Cpath d='M0 19.5 L24 19.5'/%3E%3Cpath d='M5.5 0 L5.5 24'/%3E%3Cpath d='M12 0 L12 24'/%3E%3Cpath d='M18.5 0 L18.5 24'/%3E%3C/g%3E%3Cpath stroke='%23c3bdb1' stroke-width='1.4' fill='none' d='M0 10 L24 10.6'/%3E%3Cpath fill='%2310304f' d='M24 0 L24 11 L13 0 Z'/%3E%3Cpath fill='%232e7d32' d='M24 4 L24 9 L19 4 Z'/%3E%3Cpath stroke='%23fff' stroke-width='0.8' fill='none' d='M13 0 L24 11'/%3E%3C/svg%3E");
}

/* ==== The Mapbox wordmark in the satellite credit (ROADMAP Task 489) ========================
   MAPBOX'S LOGO IS A LICENCE TERM, NOT A COURTESY: *"Maps using Mapbox map designs, data or
   software usually must display the Mapbox logo and text attribution."* Raster tiles from
   api.mapbox.com/v4/mapbox.satellite are Mapbox-supplied data, so it applies to the satellite
   basemap. It rides in #lpn_basemap_credit's satellite set beside the three text links, which is
   where the strip already is; their docs put the logo bottom-left and the text bottom-right and
   allow either to be repositioned, so one strip carrying both is inside the terms and is one less
   thing to keep on screen.

   **EMBEDDED, NEVER FETCHED.** The mark is a data: URI here rather than an <img> pointing at
   api.mapbox.com, because a request to Mapbox from a page whose visitor has not turned satellite
   on is exactly what #lpn_basemap_teaser was built to avoid -- the teaser draws its own thumbnail
   for the same reason. The artwork is Mapbox's own, taken verbatim from mapbox-gl.css (the
   dark-on-light variant, which is the one for our translucent white strip); their terms forbid
   restyling it, so nothing here recolours it. 65px is the width their attribution page states,
   and 17px is the height that keeps the 88:23 artwork's aspect ratio exactly. */
.lpn-mapbox-logo {
	display: inline-block; vertical-align: middle;
	width: 65px; height: 17px;
	background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E");
	background-size: 100% 100%;
	background-repeat: no-repeat;
}

/* ==== The Labels panel's column headings (ROADMAP Task 435) ==================================
   The four column headings must sit over the controls they name. They did not, and border-box
   (the earlier pass) was only half the cause: BOOTSTRAP'S REBOOT GIVES EVERY FORM CONTROL
   `font-size: inherit`, so an <input> here resolves `em` against the body's 1rem, while
   columnHeadings() draws its heading row at `font-size: 0.85em` and its cells resolve the SAME
   declared `2.6em`/`3.2em` against 0.85rem. Each heading is then ~15% narrower than its control,
   the leftmost flex spacer absorbs the whole shortfall, and every heading slides RIGHT -- worst at
   the left, nearly right at the right, which is exactly the residual Tom reported.

   So the widths are restated in `rem`, which is the same absolute length the controls already
   render at (body font-size is Bootstrap's 1rem) and is immune to whatever font-size the row
   carries. `!important` is not decoration: looped-network.js sets these widths inline.
   The font-size anchor on the two containers is what makes the equality hold BY CONSTRUCTION
   rather than by the body happening to be 1rem.

   The selectors are the columns themselves: child 1 is the flexible name/spacer, children 2-5 are
   Before, After, 0.000 (decimals) and Rank (priority) in both row types. `span:nth-child(2|3)`
   therefore matches only a heading cell (a field row carries an <input> there), and `span:nth-child(4|5)` matches a
   heading cell or the reserved-column spacer -- which needs the same treatment for the same reason
   and lands on the same 3.2rem it already had. Keep in step with labelCheckbox() and
   columnHeadings() in js/looped-network.js if a column is ever added. */
#lpn_labels_node_fields,
#lpn_labels_link_fields { font-size: 1rem; }
#lpn_labels_node_fields > div > span:nth-child(2),
#lpn_labels_node_fields > div > span:nth-child(3),
#lpn_labels_link_fields > div > span:nth-child(2),
#lpn_labels_link_fields > div > span:nth-child(3) { width: 2.6rem !important; }
#lpn_labels_node_fields > div > span:nth-child(4),
#lpn_labels_node_fields > div > span:nth-child(5),
#lpn_labels_link_fields > div > span:nth-child(4),
#lpn_labels_link_fields > div > span:nth-child(5) { width: 3.2rem !important; }
/* AND NOTHING IN A ROW MAY SHRINK. In the Labels pull-down the lists were as wide as they liked;
   in the Visibility panel (Task 427) they are inside a panel the user can drag narrow, and a flex
   item's default is to shrink. The heading cells are `flex: 0 0 auto` already, the <input> columns
   were not, so a narrow panel squeezed the boxes and left every heading standing 60 px to their
   right -- the exact defect Task 435 had just finished fixing, re-created by the move. The panel
   scrolls sideways instead: a column that cannot be read is worse than a scrollbar. */
#lpn_labels_node_fields > div > input,
#lpn_labels_link_fields > div > input { flex-shrink: 0; }
/* **AND THE NAME MUST BE ABLE TO WRAP ANYWHERE.** A flex item's floor is its MIN-CONTENT width, so
   "Roughness, C" held its row 6 px wider than the list itself -- and a row wider than its container
   puts every one of its four controls 6 px right of the heading that names it, which is the Task
   435 defect arriving from the opposite direction. The heading row cannot do this (its name cell is
   empty), so the two disagree by exactly the overflow. Letting the longest name break mid-word costs
   a hyphen-less line break and guarantees the columns line up at ANY width, which is what the box
   became once it was halved. CLAUDE.md: mid-word wrap is acceptable; column width is king. */
#lpn_labels_node_fields > div > label,
#lpn_labels_link_fields > div > label { min-width: 0; overflow-wrap: anywhere; }
/* A FLOOR ON THE WHOLE LIST, not `width: max-content` on each row. The name column is `flex: 1 1
   auto`, so it takes whatever the row has left -- which is only the same number on every row while
   every row is the same width. Sizing rows to their content instead gives each one a different
   name column and staggers all four headings again, worse than before. One floor for the list.

   **13rem, AND THAT NUMBER IS WHY THE SETTINGS BOX HAD A HORIZONTAL SCROLLER** (Tom, 2026-08-19:
   "It should not"). The floor was 18.5rem = 296 px. The content pane is 316 px, its sub-bodies are
   indented 8 px, and a browser that draws a CLASSIC 15 px scrollbar rather than an overlay one
   leaves 293 px -- three pixels short, permanently, on the one list every visitor opens. Nothing
   headless could see it: a headless Chromium's scrollbars are overlays and take no width, so the
   box measured clean while Tom looked straight at the scroller.
   dev/browser-pass/specs/labelcols.js now measures the pane with 56 px TAKEN AWAY, which asks the
   real question -- can this content shrink? -- instead of the accidental one.
   13rem clears the four columns (11.6rem) and their gaps with room for a name column; below that
   the names break mid-word, which the rule above allows and which is the correct last resort. */
#lpn_labels_node_fields,
#lpn_labels_link_fields { min-width: 13rem; }
/* **AND A CEILING ON THE NAME, so the columns stop walking right** (Tom, 2026-08-21: "the Labels
   columns always float right. It seems like there should be something like a maximum width for the
   checkbox and labels column. Map appearance and Time seem to do it right"). They do, and this is
   the same fix they got: the name is `flex: 1 1 auto`, so every pixel the panel is widened by went
   to the name and carried the four boxes out to the right edge with it. With a ceiling the name
   stops growing, the boxes stay where the reader last saw them, and the spare width goes to nobody
   -- which is what a settings dialog does.

   11.5rem is --lpn-set-name, the ceiling on the name column of every other row in this box, so a
   labels row and a settings row now share ONE x for their controls rather than each having its
   own. The heading row's lead cell takes it too, or the headings would stagger against the boxes
   they name -- the defect this list has been fixed for twice. */
#lpn_labels_node_fields > div > label,
#lpn_labels_link_fields > div > label,
#lpn_labels_node_fields > div > span:first-child,
#lpn_labels_link_fields > div > span:first-child { max-width: 11.5rem; }
/* **AND WHEN THERE IS NO ROOM, THE FOUR COLUMNS DROP AS A BLOCK -- THE NAME NEVER WRAPS INSTEAD**
   (Tom, 2026-08-24: "the labels are wrapping when what we want is for the four columns to wrap as a
   block under the checkbox and label. Like the phone.").

   The mid-word break the rules above allow is the LAST resort, and it had become the first: a name
   is what the reader scans down, and breaking "Roughness, e" across two lines to keep four number
   boxes on one is the wrong thing to protect. Below the same 24rem where .lpn-set-row gives up its
   second column, a field row wraps instead -- name on its own line, then its four columns beneath
   it, in the same order and at the same x as every other row's.

   `flex-basis: 100%` on the name is what forces the break, and it is why the row needs `flex-wrap`
   at all; the four columns stay direct children (the heading selectors above address them by
   position, and three separate fixes have gone into keeping headings and boxes on one x), so they
   move as a block because nothing else can fit beside them, not because they are boxed together.
   The heading row's lead cell is EMPTY, so it is removed rather than given a blank line of its own:
   its four headings then sit at the same left edge as the four boxes below them, which is the whole
   invariant this list is held to.

   The same container query as .lpn-set-row, deliberately -- one width at which this box changes
   shape, not two. A browser without container queries keeps today's behaviour, which is the safe
   direction. */
@container lpnset (max-width: 24rem) {
	#lpn_labels_node_fields > div,
	#lpn_labels_link_fields > div { flex-wrap: wrap; }
	/* `!important` is not decoration, for the reason stated above the heading widths: labelCheckbox()
	   sets `flex: 1 1 auto` INLINE on the name, and an inline shorthand carries a flex-basis of
	   `auto` that a stylesheet longhand cannot outrank. */
	#lpn_labels_node_fields > div > label,
	#lpn_labels_link_fields > div > label { flex-basis: 100% !important; max-width: 100%; }
	#lpn_labels_node_fields > div > span:first-child,
	#lpn_labels_link_fields > div > span:first-child { display: none; }
	/* **AND THE WHITE SPACE GOES ABOVE THE NAME, NEVER BELOW IT** (Tom, 2026-08-31, from a phone:
	   "the toggles and labels ... have a little gap below them from the row they belong with, but no
	   gap above them from the row they don't belong with. This would be good to reverse.").

	   Measured in Chromium at 360x740 before the fix: 6.00px between a name and its OWN four
	   columns, 0.00px between those columns and the NEXT row's name. Once a row wraps, the inline
	   `gap: 6px` labelCheckbox() writes becomes a row-gap as well as a column-gap, and the rows
	   themselves carry no margin -- so every pixel of separation in the list was sitting inside a
	   group and none of it between groups. Proximity then says the checkbox belongs to the boxes
	   ABOVE it, which is the opposite of what it controls.

	   The 6px is moved rather than added to: row-gap 0 inside the group, 6px margin between rows.
	   `!important` because the gap is an INLINE shorthand and a stylesheet longhand cannot outrank
	   one -- the same reason the flex and the widths in this block carry it. `column-gap` is
	   untouched, so nothing horizontal moves and no column widens.

	   Scoped to the two blocks where the row WRAPS. Where it does not -- the desktop box at its
	   normal width -- the name shares its line with its columns, there is no row-gap to reverse,
	   and the layout does not move. Measured at 1280x900 before and after: identical to the pixel.

	   `:not(:first-child)` rather than `div + div`, which is the same set of rows here and would
	   read better: the sibling combinator is the one selector shape
	   dev/lpn-spike/small-screen-harness.js cannot parse, so it would land in that file's
	   blind-spot report and this rule would go unasserted. */
	#lpn_labels_node_fields > div,
	#lpn_labels_link_fields > div { row-gap: 0 !important; }
	#lpn_labels_node_fields > div:not(:first-child),
	#lpn_labels_link_fields > div:not(:first-child) { margin-top: 6px; }
}

/* ==== No window scrollbar on the map page (ROADMAP Task 432) =================================
   Tom, 2026-08-18: *"Our bottom controls bar should be the hard bottom of the page."* This is a
   full-window drawing surface; anything that scrolls the WINDOW moves the whole application, so
   the window does not scroll at all and the map footer is the bottom of the world.

   SCOPED BY WHAT ONLY THIS PAGE HAS. <body> carries no page class, and the other fifteen
   calculators are a form and an answer and MUST keep scrolling normally -- so the scope is the map
   canvas itself. A browser without `:has()` simply drops the rule and behaves as before, which is
   the safe direction.

   `overflow: hidden` on the root is what the viewport reads; body is left alone. The two lines
   below do different jobs and both are wanted:
     - applyMapHeight() already fits the canvas to the window, but it measures body's own box, and
       a bottom margin on the last in-flow block collapses OUT of that box and into the document's
       scroll height. That is one stubborn pixel this page can never see and never fix, and it is
       enough for a scrollbar. Zeroing it removes the overflow rather than hiding it.
     - `overflow: hidden` then guarantees the invariant instead of re-deriving it: it also kills the
       scrollbar during load, while the canvas is still wearing its 10000px curtain.

   THE COST, stated rather than discovered: below LPN_MAP_MIN the canvas is allowed to overflow on
   purpose (a window too short for a real map), and what overflows is now clipped instead of
   scrollable. That is the request. A panel that needs to scroll scrolls INSIDE itself --
   .lpn-popover-body already does. */
html:has(#lpn_canvas) form#formInput { margin-bottom: 0; }
html:has(#lpn_canvas) { overflow: hidden; }

/* THE RUN BOX (ROADMAP Task 450). Tom, 2026-08-19: "The Run button does nothing... It needs a box
   with a progress bar and completion report." It is built and appended to <body> by js/lpn-time.js,
   which is what keeps the whole feature out of js/looped-network.js and Looped-Network.php.

   BOTTOM LEFT, not bottom right: the right edge of the map is where .lpn-rpane lives, and a box
   that lands on top of the properties pane covers the numbers the run just produced. Fixed rather
   than placed near the button, because a run takes seconds and the eye will have moved.

   z-index joins the movable boxes at 1200 rather than sitting at its old 1085 (2026-09-02). It is
   made draggable by makePanelDraggable() like every other box, so a stack that left it below them
   meant the one box reporting a run in progress could be buried by a box opened over it -- and at
   equal specificity the later rule wins, so its own 1085 was quietly beating .lpn-dragpanel's 1200.
   raisePanel() writes a higher number inline as boxes are touched; this is the floor. Still below
   the registration bar (3000), which must outrank everything while a click sequence is in
   progress. */
.lpn-runbox {
	position: fixed; left: 1em; bottom: 1em; z-index: 1200;
	box-sizing: border-box; width: 26rem; max-width: calc(100vw - 2em);
	padding: 0.5em 0.75em 0.6em; border: 1px solid #ccc; border-radius: 4px;
	background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, .2); font-size: 0.9em;
}
.lpn-runbox-head { display: flex; align-items: center; gap: 0.5em; }
.lpn-runbox-title { font-weight: bold; flex: 1 1 auto; }
/* The percentage is a bare number and a percent sign, so it needs no width reserved for a longer
   translation -- there is no translation. Tabular figures stop it jittering as it counts up. */
.lpn-runbox-pct { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: #555; }
.lpn-runbox-x {
	flex: 0 0 auto; width: 1.8em; height: 1.8em; line-height: 1;
	padding: 0; border: 0; background: transparent; font-size: 1.1em; color: #333; cursor: pointer;
}
.lpn-runbox-x:hover, .lpn-runbox-x:focus { background: #eee; color: #000; }
.lpn-runbox-msg { margin: 0.25em 0 0.4em; }
.lpn-runbox-bar { height: 0.5em; background: #e6e6e6; border-radius: 3px; overflow: hidden; }
/* No transition on the width. The fill is driven by EPANET's own clock at the end of each slice,
   so an animation would be a second, slower, made-up progress bar drawn over the real one. */
.lpn-runbox-fill { height: 100%; width: 0; background: #1976d2; }
/* THE DOOR TO THE REPORT, not the report (ROADMAP Task 570). The .rpt used to be a `<details>`
   with its own `<pre>` and its own Copy inside this PROGRESS box; it has a draggable, sizeable box
   of its own now (.lpn-rptbox-pre, above), and what is left here is one button that opens it.
   Quiet by default: it is an offer beside a progress bar, not a command competing with the box's
   close button. */
.lpn-runbox-reportbtn {
	margin-top: 0.4em; font: inherit; font-size: .85em;
	padding: 1px 8px; cursor: pointer; background: none; border: 1px solid var(--rule, #999);
	border-radius: 3px; color: inherit;
}
.lpn-runbox-reportbtn:hover, .lpn-runbox-reportbtn:focus { background: #eef3fd; }
/* ---- THE LIBRARIES BOX (ROADMAP Tasks 462 and 460) -------------------------------------------

   It borrows the Settings box's whole shell -- .lpn-popover for the size caps, .lpn-setbox for the
   drag band, the resize grabber and the two panes -- so there is exactly one box design on this
   page and a second one cannot drift from the first. What is different is only what these three
   editors need and a settings row does not: a series of numbers wide enough to read, a chart beside
   it, and a sentence that wants the width of the box.

   WIDER THAN SETTINGS, AND THAT IS THE WHOLE OVERRIDE. A settings row is a name and one small
   control; a control sentence is `LINK 335 OPEN IF NODE 1 BELOW 17.1` and a 24-point pattern is a
   line of numbers. Both are read left to right and neither wraps usefully, so the box opens at
   44rem against Settings' 34. Everything else -- the height, the minimums, the resize -- is
   inherited deliberately. */
.lpn-libbox { width: min(44rem, 94vw); }
/* ---- THE FIRE FLOW BOX (ROADMAP Task 530) -----------------------------------------------------
   It borrows .lpn-setbox's whole shell -- moveable by its chrome, resizeable by `resize: both` and
   by the touch grabber, capped to the viewport, body scrolling inside itself. Only the width is its
   own: two report tables side by side in the reading want more room than the Settings index needs,
   and less than the Library's three editors. */
.lpn-ffbox { width: min(52rem, 94vw); }
/* ---- THE RUN'S OWN DIALOG (ROADMAP Task 530) --------------------------------------------------

   Progress, Stop, nothing else (Tom, 2026-08-30). It borrows .lpn-popover for the viewport caps and
   .lpn-setbox-title for the drag band, and it deliberately does NOT borrow .lpn-setbox: that shell
   opens at a fixed min(46rem, 92vh), which for a bar and a button would be a mostly-empty dialog
   covering the drawing it is reporting on. It is as tall as its own three lines.

   NARROW ON PURPOSE. On a phone this is most of the screen whatever we do, so what matters is that
   it is not ALSO tall: 22rem holds "Working: 47 of 225 junctions." on one line in English and wraps
   harmlessly in a longer language. */
.lpn-ffrunbox { width: min(22rem, 92vw); }
/* The determinate bar. Its own colour rather than a system accent, because the three verdict
   colours on this page are already spoken for and a bar in one of them would read as a verdict. */
.lpn-ff-bar {
	height: 14px; border: 1px solid #999; background: #f0f0f0; border-radius: 3px;
	overflow: hidden; margin: .2em 0 .6em;
}
.lpn-ff-bar-fill { height: 100%; width: 0; background: #1565c0; transition: width .15s linear; }
.lpn-ff-controls, .lpn-ff-report { flex: 0 0 auto; }
.lpn-ff-report { overflow: auto; min-height: 0; }
.lpn-ff-row { display: flex; align-items: baseline; gap: .5em; margin: .25em 0; }
.lpn-ff-row > span:first-child { flex: 1 1 auto; }
.lpn-ff-row > input, .lpn-ff-row > select { flex: 0 0 8rem; min-width: 0; }
.lpn-ff-unit { flex: 0 0 4rem; color: #555; font-size: .9em; }
.lpn-ff-head { font-weight: bold; margin: .8em 0 .3em; }
.lpn-ff-note { margin: .3em 0; font-size: .9em; color: #444; }
.lpn-ff-summary { margin: .4em 0; font-weight: bold; }
.lpn-ff-buttons { margin: .6em 0; display: flex; gap: .5em; }
/* Column width is king (CLAUDE.md): the table is read in a box a reader may have made narrow, so
   the id column wraps rather than pushing the numbers off the edge. */
.lpn-ff-tablewrap { overflow-x: auto; }
.lpn-ff-table { width: 100%; border-collapse: collapse; font-size: .9em; }
/* COLUMN WIDTH IS KING, and ten columns in one table is where that stops being a slogan: the
   headings wrap mid-word rather than widening, and only the id and the two sentence-shaped cells
   are allowed to grow. */
.lpn-ff-table th { font-weight: bold; white-space: normal; }
.lpn-ff-table th, .lpn-ff-table td { text-align: left; padding: .15em .4em; border-bottom: 1px solid #ddd; overflow-wrap: anywhere; }
/* The same three states as the rings, as a quiet tint on the row rather than as text colour: the
   verdict is already spelled out in the Result column, so this is a second channel and not the
   only one. */
tr.lpn-ff-fail { background: #fdecea; }
tr.lpn-ff-design { background: #fff4e5; }
tr.lpn-ff-error { background: #f2f2f2; }
/* **AND A NARROWER INDEX AGAIN** (Tom, 2026-08-22: "The Libraries box can have an Index pane width
   0.70 * current, at least in English"). 0.7 x the 7.5rem both boxes shipped at. It is a shorter
   list of shorter names than Settings' -- Patterns, Curves, Controls -- so it can give up more, and
   what it gives up goes to the pattern series and the control sentence, which are the two things
   this box exists to show whole. */
#lpn_library_box .lpn-setbox-index { flex-basis: 5.25rem; }
/* An entry is one pattern, one curve or one control: a block with a rule under it, so a list of
   them reads as a list of things rather than as a paragraph of controls. */
.lpn-lib-entry { padding: 6px 0 8px; border-bottom: 1px solid #eee; }
.lpn-lib-entry:last-child { border-bottom: 0; }
/* The head row: the thing's name, then its own delete, pushed to the right where a destructive
   command belongs and away from the fields the hand is aiming at. */
.lpn-lib-head { display: flex; align-items: center; gap: 6px; }
.lpn-lib-head .lpn-lib-id { width: 9em; }
.lpn-lib-del { margin-left: auto; }
/* THE CURVE EDITOR'S TWO HEADER LINES (Tom, 2026-09-05: "put pump ID (with new ID label above it)
   and Description on row/line 1 and Type selector and Equation (for pump head) on row/line 2"),
   which is the shape of EPANET's own Curve Editor. Every control carries its name ABOVE it rather
   than beside it, so the two rows line up on their fields whatever the label's language does to its
   width -- a beside-label row is the one that breaks in German and in Arabic. align-items: flex-end
   so a one-line label and a two-line one still sit their boxes on the same baseline. */
.lpn-lib-curve-row, .lpn-lib-row { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
.lpn-lib-field { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.lpn-lib-field-grow { flex: 1 1 8em; }
.lpn-lib-fieldname { font-size: .9em; opacity: .8; }
/* The fitted equation is an ANSWER, not a box to type in: read-only, in the digits' own face so it
   lines up with the table under it, and it wraps rather than widening the panel. */
.lpn-lib-equation { font-family: monospace; font-size: .9em; padding: 2px 0; overflow-wrap: anywhere; }
/* THE SENTENCE AND THE SERIES BOTH WANT THE WIDTH. A control is one line of text and a pattern's
   multipliers are one line of numbers, and in both cases the useful thing is to see the whole of it
   at once -- which is the opposite of the settings box's capped number box, and the reason these
   are here rather than there. tabular-nums so a column of digits typed into the pattern field lines
   up with the chart above it. */
.lpn-lib-wide { width: 100%; box-sizing: border-box; font-family: inherit; }
.lpn-lib-values { font-variant-numeric: tabular-nums; }
/* The sparkline. A fixed height and a fluid width: it is a SHAPE check ("is this the daily curve I
   meant?"), not a chart to read values off, so it needs no axis and no numbers. */
.lpn-lib-spark { display: block; width: 100%; height: 46px; background: #fafafa; border: 1px solid #ddd; }
/* A curve is read in two dimensions -- flow across, head up -- so it gets more height than a
   pattern, which is read only for its ups and downs along one axis. */
.lpn-lib-chart { height: 76px; }
.lpn-lib-note { font-size: .9em; opacity: .8; margin: 2px 0; }
.lpn-lib-verdict { font-size: .9em; }
/* The section a library entry list sits under. Its heading is not sticky -- unlike Settings, only
   one section is on screen at a time, so there is nothing to lose track of. */
.lpn-lib-sec { margin: 0; }
.lpn-lib-sec > h3 { margin: 0 0 4px; padding: 0 0 4px; font-size: 1.15rem; font-weight: bold; border-bottom: 1px solid #ccc; }

/* THE FIELDS AN INPUT UNIT DECIDES, ONE PER LINE (ROADMAP Task 425). Tom asked for a column rather
   than a comma list, and the reason is what the reader is doing with it: scanning for whether their
   own quantity is in the set, which is a column-reading job. Indented so the names read as items of
   the line above them rather than as more of the sentence. No bullets -- a marker per line would
   add three glyphs and no meaning to a list of five. */
.lpn-unit-fields { margin: 0.15em 0 0.7em 1.4em; }

/* Print preparation: the Printable version button. */
.engcalcs-print-tools > p { margin-bottom: 0.2em; }

/* ==== THE SMALL-SCREEN PRESENTATION PASS FOR THE MAP EDITOR (ROADMAP Task 486) ===============

   Tom, 2026-08-22, naming four concessions "for both crawlers and humans if they are on a
   mobile/small screen": hide page titles, hide or at least collapse the HawsEDC navbar, hide all
   the toolbar buttons except for the transport, and drop the menu bar to icons. His frame decides
   every judgement call below: *"the truth is that this is a PC app... For PC of course, but go
   ahead and try it on your phone."* So this is a DEFENSIBLE PHONE, not a phone-first redesign --
   the desktop layout above the breakpoint is untouched, byte for byte, and nothing here changes
   what any control does.

   THE BREAKPOINT IS 640px, AND IT IS THE ONE THIS PAGE ALREADY HAD. The project tab strip drops to
   the current tab alone at exactly this width, for exactly this reason ("A map page has no
   horizontal room to spare on a phone"). A second threshold would make the page reorganise itself
   twice on the way down, which is worse than either threshold being slightly wrong. It is a
   VIEWPORT, not a user agent: a phone is the case Tom named, but a narrow window on a laptop gets
   the same relief and no UA string has to be guessed at.

   SCOPED BY WHAT ONLY THIS PAGE HAS, the same way Task 432 scopes the no-scrollbar rule: <body>
   carries no page class, and the other fifteen calculators are a form and an answer whose titles
   and navbar must stay exactly as they are. A browser without `:has()` drops the rule and gets
   today's layout, which is the safe direction.

   WHAT IS DELIBERATELY NOT HIDDEN AT ANY WIDTH: #lpn_basemap_credit (tile attribution is a licence
   condition, not chrome) and every consent surface, which lives in the footer and is never
   selected here. */
@media (max-width: 640px) {

	/* 1. THE PAGE TITLES. The h1, the welcome line and the page description -- the same three ids
	   the "Show page titles" preference already takes away (Task 289), so a phone simply gets that
	   preference's answer without having to find the switch. Above the breakpoint the preference
	   still governs and this rule is not in play. */
	html:has(#lpn_canvas) #ec-page-title,
	html:has(#lpn_canvas) #ec-page-welcome,
	html:has(#lpn_canvas) #ec-page-desc { display: none; }

	/* 2. THE NAVBAR IS COLLAPSED, NOT HIDDEN, and Tom's own wording allows either ("hide or at
	   least collapse"). Bootstrap has already folded it to one row here -- brand, Libre mark,
	   hamburger -- and what is behind that hamburger is the only route on the page to the other
	   fifteen calculators AND to the language picker, which on a 27-language suite is not a thing
	   to take away from the one device where a reader is most likely to be in their own country.
	   So the height goes and the routes stay: the bar's own vertical padding to nothing, the brand
	   to body size, and the "Libre Software" wordmark away (it already goes at 400px suite-wide --
	   this brings that forward on this page only, where the row is competing with a drawing
	   surface). Measured against Bootstrap's defaults this is most of the bar's height. */
	html:has(#lpn_canvas) .navbar { padding-top: 0; padding-bottom: 0; }
	html:has(#lpn_canvas) .navbar-brand { font-size: 1rem; padding-top: 0.1rem; padding-bottom: 0.1rem; }
	html:has(#lpn_canvas) .ec-nav-libre { display: none; }

	/* 3. THE TOOLBAR KEEPS THE TRANSPORT AND NOTHING ELSE. `.lpn-transport-btn` is the class
	   js/lpn-time.js puts on the three player controls and only on them, so this is the same line
	   the toolbar's own styling already draws between a player and an icon -- not a second, parallel
	   list of buttons that could drift from it.

	   EVERY BUTTON THIS HIDES HAS A MENU: Open/Save/Save as in File, the seven Insert tools and
	   Select/Undo/Find/Libraries/Delete in Insert and Edit, Zoom to fit in View, and
	   Settings/Profile/Tables/Run in Project. Select had none until Task 486 added the Edit row --
	   see openEditMenu(). The one control with no exact menu twin is the bottom-pane TOGGLE, and
	   the pane is still opened by Project > Tables or Project > Profile and closed by its own X.

	   THE TWO <select>s STAY. They are not buttons, and the step selector is the only control on
	   the page that says which moment is showing -- a transport you cannot read is not a transport.
	   Run is a button and is not a player control (js/lpn-time.js is explicit about that), so it
	   goes; on a project with auto-run on, which is the default, it is already hidden anyway.

	   The run group then has to give up the divider and the gap it wears as a MIDDLE group, or the
	   strip would open with a rule and 12px of air in front of its first control. */
	html:has(#lpn_canvas) #lpn_toolbar > .lpn-toolbar-group:not(#lpn_toolbar_run) { display: none; }
	html:has(#lpn_canvas) #lpn_toolbar_run button:not(.lpn-transport-btn) { display: none; }
	html:has(#lpn_canvas) #lpn_toolbar_run { border-left: 0; padding-left: 0; margin-right: 0; }

	/* 4. THE MENU BAR DROPS TO ICONS. The word is a <span class="lpn-menubar-word"> that
	   buildMenuBar() builds for this rule to reach; the accessible name is on the button as an
	   aria-label at every width, so what goes is the ink and not the name. Six icons instead of six
	   words is the difference between a menu bar that fits a phone and one that wraps to two lines
	   above a map. */
	html:has(#lpn_canvas) .lpn-menubar-word { display: none; }
	/* An icon alone in a box padded for a word reads as a gap with a drawing in it. */
	html:has(#lpn_canvas) .lpn-menubar-item { padding: 3px 6px; }

	/* **5. THE SETTINGS INDEX STAYS A NARROW SIDE COLUMN ON A PHONE, RESTORED 2026-08-29.**

	   **AND SINCE 2026-08-30 IT IS A TRANSFER, WHICH IS NOT THE SAME THING AS A RATIO** (Tom, having
	   used the box once the overrun was fixed: *"the main (left) pane only needs 75-80 percent of the
	   width it has... I think we should try giving the index pane 20% of the main pane's width"*, and
	   then, on being shown the other reading: *"No. I meant for you to rob 20% of the then-present
	   width of the main pane and give that to the index pane."*). **The index does not REACH 20% of
	   anything.** It keeps the width it already had and gains a fifth of the main pane on top; the
	   main pane keeps four fifths of what it had. Do not "simplify" this to a flat percentage -- a
	   flat 20% is the reading he rejected in those words.

	   The arithmetic, with P the panes' inner width and g the 10px gap:
	       index_now   = 4.5rem                        (the figure this rule carried before)
	       content_now = P - g - 4.5rem
	       index_new   = 4.5rem + 0.20 x content_now
	                   = 4.5rem + 0.2P - 0.2g - 0.9rem
	                   = 3.6rem + 0.2P - 2px
	   which is the calc below. A percentage flex-basis resolves against the flex container's inner
	   main size, and the gap is NOT deducted from it -- so the `- 2px` is 20% of the gap and is
	   exact rather than a fudge. `content_new = 0.8 x content_now` then falls out of the content
	   pane's own `flex: 1 1 auto`; it is not restated anywhere.

	   Measured in Chromium at 360x640, where P is 320.4 (94vw less 16px of padding and 2 of border):
	   the index goes 72 -> 119.7 and the main pane 238.4 -> 190.7, which is 0.800 of what it had.
	   **THE CONSEQUENCE, MEASURED RATHER THAN FEARED:** 4.5rem was chosen as the width at which the
	   CONTENT pane stopped being the constraint, and 190.7 is below it -- so the pane was re-measured
	   at the new width instead of being argued about. Exactly ONE thing in it overflows, by 17px, and
	   it is not a setting: it is the colour-ramp attribution footer, whose lines carry a bare URL
	   that no amount of pane width would have broken. It is given `overflow-wrap` at 5a below. Every
	   settings row, and the node symbology list that set the old floor, still fits.

	   **IT WAS TURNED INTO A HORIZONTAL TAB STRIP ON 2026-08-25 AND TOM REVERSED THAT** (2026-08-29:
	   *"It was good before with the right pane index. It's bad with top tabs."*). The strip was
	   argued for on the grounds that a phone should read whole words rather than wrapped ones; his
	   answer is that a scrolling row of tabs costs more than a wrapped word, because an index you
	   scroll sideways is an index you cannot see. **Do not re-propose the strip** -- it was built,
	   shipped, used and rejected.

	   **THE PROBLEM THE STRIP WAS SOLVING WAS NOT AN OPEN PROBLEM** (Tom, 2026-08-29: *"We already
	   accepted broken words. And we never discussed tabs across the top."*). Eight of the fourteen
	   labels split mid-word at 65 px -- "Visualiz/ation", "Node symbo/logy" -- and that had already
	   been weighed and accepted. Task 527 reopened a settled question, answered it with a layout
	   nobody had proposed to him, and shipped it. **The cost of a wrapped word was known; the cost of
	   a sideways-scrolling index was not, and it was worse.**

	   Recorded because the failure was one of PROCESS, not of taste: the alternatives are still on
	   file (abbreviating the section names -- new English strings and 26 translations each; or ~9 px
	   type so "Visualization" fits) and either would be a change to the WORDS, which is Tom's, not
	   to the layout, which is settled.

	   ONE RULE FOR BOTH BOXES, and it reaches the Libraries box's own `#lpn_library_box` override by
	   specificity rather than by accident: `:has()` takes the specificity of its argument, so
	   `html:has(#lpn_canvas) .lpn-setbox-index` is (1,1,1) against that rule's (1,1,0). Kept
	   deliberately -- the Libraries index is a SHORTER list of shorter names (Patterns, Curves,
	   Controls) than Settings', its box is the same 94vw on a phone, and what it gives up goes to the
	   pattern series and the control sentence, which are the two things that box exists to show
	   whole. A second phone figure there would be a second pane design.

	   **AND THE TRANSFER IS THE SAME EXPRESSION FOR BOTH, WHICH IS NOT A COINCIDENCE AND IS NOT AN
	   OVERSIGHT.** A transfer is derived from the pane's PRESENT width, and the Libraries index's
	   present width on a phone is this rule's 4.5rem -- its own 5.25rem is a DESKTOP figure, already
	   overridden here. So "a fifth of the main pane, moved across" is one calculation for both boxes,
	   and deriving a second one from 5.25rem would be transferring from a width that box does not
	   have at this size.

	   The 3.6rem and the 2px are the arithmetic above, already reduced; the 2px is a fifth of
	   `.lpn-setbox-panes`'s own 10px `gap`, restated because `calc()` cannot ask for it. */
	html:has(#lpn_canvas) .lpn-setbox-index { flex-basis: calc(3.6rem + 20% - 2px); }

	/* 5a. AND THE ONE THING THE NARROWER MAIN PANE NO LONGER FITS. Measured at 360px after the
	   transfer above: the content pane overflows by exactly 17px, and every pixel of it is the
	   colour-ramp attribution footer, whose credit lines end in a bare URL -- 208px of unbreakable
	   token in a 191px pane. The same answer the index rows got, and for the same reason: a broken
	   address is still an address, and the licence asks for the text to be readable, not unwrapped.
	   PHONE ONLY -- at every wider width it has always fitted on one line, which is how it reads
	   best. */
	html:has(#lpn_canvas) .lpn-rp-credit { overflow-wrap: anywhere; }

	/* 5b. THE BOTTOM-PANE TABLES' NUMBER BOXES (Tom, 2026-08-22: "The Tables in the bottom pane have
	   some columns, inputs I think, with artificially limited shrink that can in general be about
	   50% of current"). `.lpn-pane-table input { width: 7em }` is the only imposed width anywhere in
	   those tables -- every other column is a heading button, a plain cell or an ID link and is
	   already as narrow as its own content -- so it is the only thing there is to halve. 3.5em holds
	   five digits at this font, and an input scrolls its own content rather than truncating it, so a
	   longer number is typed and read normally in a shorter box.

	   BELOW THE BREAKPOINT ONLY. Tom's "in general" may well have meant at every width; the standing
	   rule is that the desktop layout does not move unless he asked for that layout to move, and
	   this item did not say so. Moving it out of this block is a one-line change if he confirms. */
	html:has(#lpn_canvas) .lpn-pane-table input { width: 3.5em; }

	/* 5c. AND THE PIPES TABLE'S TWO WIDEST HEADINGS COME IN WITH THEM (Tom, 2026-08-23: "Pipes is
	   the table with too much for a narrow screen, and it's not much more than a narrow screen can
	   handle... Scale the width of the Roughness input 0.75 and column 0.6, Minor loss Km input 0.5
	   and column 0.5 (approx.), just to help a little"). Ten columns of pipe measure 602px in a
	   360px panel; these two were 100.4px and 56.4px of it.

	   WRAP, NEVER ABBREVIATE. An abbreviated heading is a new English string and 26 translations of
	   it, and mid-word wrap in a column heading is already the house rule. `overflow-wrap: anywhere`
	   is what lets the declared width win: a table column can never be narrower than its own
	   min-content, and without it "Roughness" is one unbreakable 60px word.

	   MINOR LOSS STOPS AT 0.74 RATHER THAN THE 0.50 ASKED FOR, AND THE REASON IS VERTICAL. Its
	   heading is the one that has to break mid-word to get narrow, and every break is another line
	   in a heading row inside a pane 260px tall: at 0.50 the column is 31px and "Minor loss, k"
	   becomes SIX lines, 129.7px against the 86.4px "Head loss (ft H2O)" already costs. 2.9em is the
	   last width that still fits in four -- so the row is no taller than it was, and the 43px this
	   saves vertically is worth more than the 14px it gives up sideways. The input inside it is the
	   0.5 Tom asked for either way. Sideways scroll stays the fallback: this is a nudge, not a
	   re-layout, and the table is still wider than the phone. Measured on Elm Street Center at
	   360px: Roughness 100.4 -> 60.5, Minor loss 56.4 -> 41.8, the table 602 -> 547. */
	/* ROUND 3 (Tom, 2026-08-23) WIDENED BOTH AGAIN, and the two asks landed differently here.
	   Minor loss he scaled "1.25 to 1.5 on PC and phone", so the phone box takes the same 1.4 the
	   desktop one took: 2.19 -> 3em, and the column with it, 2.9 -> 3.7em. Widening that column
	   only REDUCES the heading lines the note above counted, so the vertical argument still holds.

	   ROUGHNESS STOPS AT THE PHONE DEFAULT RATHER THAN THE 2.5 ASKED FOR. He named no platform for
	   it, unlike Diameter's explicit "on PC", so it is meant to move here too -- but 2.6 x 2.5 is
	   6.5em, which is most of a 360px panel and would undo the phone ruling he made the day before
	   for the opposite reason. 3.5em is where the two meet: it is the width every other box in
	   these tables already has below the breakpoint, so Roughness stops being the one column
	   NARROWER than the default, and an input scrolls its own content, so a long e is still typed
	   and read normally. Say so if the full 2.5 is wanted on a phone as well. */
	/* ROUND 4 (Tom, 2026-08-25, from a phone) BUYS THE WORD BACK AND PAYS 28 px FOR IT. The Pipes
	   heading read "Roughnes/s, C"; the heading wants to break at its comma, "Roughness," over "C",
	   and could not, because `overflow-wrap: anywhere` lets a column be narrower than its own longest
	   word and round 3's 5em is exactly that.

	   THE PRICE IS NOT NEGOTIABLE AND IT WAS MEASURED, not reasoned: "Roughness," is 94.4 px at this
	   cell's own bold 14.4 px, so ANY layout that keeps the word whole makes this column 100.4 px --
	   which is what it measured before round 2 narrowed it. There is no width between 72 and 100 that
	   helps; 5.9em and 6.4em were both tried and both still split the word. So the choice is the word
	   or the 28 px, and Tom's 2026-08-25 ruling is the later one: he read "Roughnes/s" and called it a
	   defect. The table is 547 -> 575 px in a 360 px panel and was already scrolling sideways.

	   `break-word`, NOT the removal of the property: it keeps the last-resort split for a language
	   whose single word is longer than the panel, while putting the column's floor at the longest
	   word instead of at one character. The km column keeps `anywhere` -- its longest word is "Minor"
	   at ~34 px in a 47 px column, so it has never split and nothing there is being paid for. */
	html:has(#lpn_canvas) .lpn-pane-table .lpn-pane-col-roughness {
		overflow-wrap: break-word; width: 5em; min-width: 5em;
	}
	html:has(#lpn_canvas) .lpn-pane-table .lpn-pane-col-roughness input { width: 3.5em; }
	html:has(#lpn_canvas) .lpn-pane-table .lpn-pane-col-km {
		overflow-wrap: anywhere; width: 3.7em; min-width: 3.7em;
	}
	html:has(#lpn_canvas) .lpn-pane-table .lpn-pane-col-km input { width: 3em; }

	/* 6. THE SYMBOLOGY ROWS WRAP AS A GROUP (Tom, 2026-08-22, choosing between his own two ideas:
	   "Idea B is to wrap the four inputs for Before/After/Decimal/Drop together below the checkbox
	   and label, and this is a better balance of width savings. So for the first row we would have
	   them wrap gracefully to `checkbox ID\nbefore after decimal drop`"). Idea A -- the label first
	   and everything else wrapping under it individually -- he weighed and rejected.

	   THE FOUR STAY ONE GROUP because the label takes a whole line to itself (`flex-basis: 100%`)
	   and nothing else can share it. Wrapping them individually would let a row break between After
	   and Decimal, and a column that is on line two of one row and line one of the next is not a
	   column at all.

	   The name's 11.5rem ceiling goes with it: that ceiling exists to stop the four boxes walking
	   right as the box widens, and there is nothing to walk right of once they are on their own
	   line. `!important` on the flex for the same reason as the widths below: labelCheckbox() writes
	   `flex: 1 1 auto` inline, and an inline declaration beats a stylesheet one.

	   THE HEADING ROW'S LEAD CELL GOES, so the headings start at the left edge exactly where the
	   wrapped group does. `display: none` does not renumber :nth-child, so the width rules below --
	   and the ones above, which they override -- still count the lead as child 1. */
	html:has(#lpn_canvas) #lpn_labels_node_fields > div,
	html:has(#lpn_canvas) #lpn_labels_link_fields > div { flex-wrap: wrap; }
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > label,
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > label { flex: 1 1 100% !important; max-width: none; }
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > span:first-child,
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > span:first-child { display: none; }

	/* 6a. **AND THE WHITE SPACE GOES ABOVE THE NAME, NEVER BELOW IT** (Tom, 2026-08-31, on a phone:
	   "the toggles and labels ... have a little gap below them from the row they belong with, but no
	   gap above them from the row they don't belong with. This would be good to reverse.").

	   Stated once beside the container-query copy of item 6 above, which carries the measurement and
	   the reasoning; it is repeated here for the same reason every other rule in item 6 is -- a
	   browser without container queries must still get the phone layout. Same declarations, so the
	   two firing together is idempotent. */
	html:has(#lpn_canvas) #lpn_labels_node_fields > div,
	html:has(#lpn_canvas) #lpn_labels_link_fields > div { row-gap: 0 !important; }
	html:has(#lpn_canvas) #lpn_labels_node_fields > div:not(:first-child),
	html:has(#lpn_canvas) #lpn_labels_link_fields > div:not(:first-child) { margin-top: 6px; }

	/* 7. AND THE COLUMNS THEMSELVES SHRINK (Tom: "Let's get brave and use these shrink factors on
	   the column inputs: Before 0.8, After 0.4, Decimal 0.8"). Against the shipped 2.6rem affix and
	   3.2rem numeric columns that is 2.08 / 1.04 / 2.56 / 2.56rem. `!important` because
	   labelCheckbox() writes these widths inline on the <input>s; the heading <span>s are matched by
	   the same selectors so a heading can never come away from the box it names.

	   ON THE HEADINGS Tom offered three answers and this takes two of them: (a) the narrow widths
	   only on the small screen, and (c) live with the wrapping. Not (b), abbreviations -- four new
	   English strings and 104 translations for a phone-only cosmetic gain, against a house rule that
	   already says mid-word wrap in a column heading is acceptable and column width is king. */
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > :nth-child(2),
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > :nth-child(2) { width: 2.08rem !important; }
	/* **EXCEPT After, WHICH CAME BACK TO 1.6rem** (Tom, 2026-08-23, after using the phone layout:
	   "After 2.0 (make it like Decimal and Drop)"). The two instructions do not agree and the
	   PARENTHESIS is the one to follow: 2.0 x 1.04 = 2.08rem would make After the WIDEST of the four
	   on a touch screen, where Decimal and Drop are 1.6rem -- the opposite of "like" them. So it
	   lands on their number.

	   1.6rem is ~26 px, which holds two characters of content once the input's own padding and
	   border are paid -- enough for a bare `ft` or `m`, and a longer affix scrolls inside its box
	   rather than being cut off. At 1.04rem it could not show one. */
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > :nth-child(3),
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > :nth-child(3) { width: 1.6rem !important; }
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > :nth-child(4),
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > :nth-child(5),
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > :nth-child(4),
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > :nth-child(5) { width: 2.56rem !important; }
	/* 7a. **AND A HEADING THAT DOES NOT FIT ITS COLUMN MUST WRAP INSIDE IT, NOT SPILL OUT OF IT**
	   (ROADMAP Task 527). Tom, 2026-08-25, from a phone: the four headings read "BeforeAfter 0.000
	   Drop", touching each other and out of line with the boxes beneath them. The widths above were
	   applied and are exactly right -- measured at 360 px, every heading's BOX sits on its control's
	   box to a tenth of a pixel. What is wrong is the ink: "Before" is one unbreakable word about
	   40 px wide in a 33 px column, and with nothing to break it a span simply paints past its own
	   edge and into its neighbour.

	   So the wrapping the note above chose -- Tom's option (c), against (b) abbreviations -- was
	   never actually reachable, because a word with no break opportunity does not wrap. This is the
	   one property that gives it one. Mid-word wrap in a column heading is the house rule (CLAUDE.md:
	   column width is king), and it applies here in a way it does not to the Settings index above:
	   these are four columns of a table, not a list of destinations.

	   THE HEADING ROW ONLY, matched as the list's first child, so the inputs below keep the sizing
	   they have. */
	html:has(#lpn_canvas) #lpn_labels_node_fields > div:first-child > span,
	html:has(#lpn_canvas) #lpn_labels_link_fields > div:first-child > span { overflow-wrap: anywhere; }

	/* The list's own floor comes down with them, or the box would still refuse to narrow past a
	   width the rows no longer need. 11rem is the four boxes (8.8rem, After included at its new
	   1.6rem) plus their three gaps and two margins; the name above them wraps between words. */
	html:has(#lpn_canvas) #lpn_labels_node_fields,
	html:has(#lpn_canvas) #lpn_labels_link_fields { min-width: 11rem; }

	/* 7b. **AND THE NAME ON THAT FIRST LINE NEVER BREAKS MID-WORD** (Tom, 2026-08-23, drawing three
	   panels: the roomy desktop row, then the cramped one reading `Dem/and`, `Hea/d`, `Pres/sure`,
	   `Elev/ation`, then what he wants -- the checkbox and the whole name on line 1, the four inputs
	   on line 2).

	   The mid-word break is `overflow-wrap: anywhere` on the label, and that rule is RIGHT where it
	   was written: on the desktop the name shares its line with the four columns, and a name whose
	   min-content width exceeds its share pushes every column out of line with its heading (Task
	   435, twice). None of that survives item 6 above -- here the name is `flex: 1 1 100%` and owns
	   the whole row -- so the reason for breaking anywhere is gone and only the ugliness is left.

	   `normal`, NOT `nowrap`. Every English name fits its line at 360 px, which is the layout Tom
	   drew; the longest TRANSLATION does not -- "Gradijent gubitka tlačne visine" (hr, 31 characters;
	   ro and id are the same length) wants ~230 px against the ~205 px the content pane leaves at
	   that width. `nowrap` would answer that with the sideways scrollbar this box has twice been
	   narrowed to get rid of, on the one list every visitor opens. A break BETWEEN WORDS is not the
	   defect he objected to: `Gradijent gubitka` over `tlačne visine` is still whole words, and it
	   costs nothing in any language whose name already fits on the line. */
	html:has(#lpn_canvas) #lpn_labels_node_fields > div > label,
	html:has(#lpn_canvas) #lpn_labels_link_fields > div > label {
		overflow-wrap: normal; word-break: normal;
	}

	/* 8. NO SPINNER ON A TOUCH SCREEN, AND THE WIDTH THAT PAID FOR IT COMES BACK (Tom: "since the
	   spinner is not available on touch screen, drop the spinner on touch screen and on touch screen
	   CSS only"). `.ec-spin` exists to put the native up/down arrows BACK on these two columns after
	   the suite strips them everywhere else, and a touch keyboard draws no arrows to put back -- so
	   on a coarse pointer the column is paying ~17 px for furniture that is not there.

	   **A MEDIA QUERY IS A VIEWPORT TEST, NOT A TOUCH TEST**, which is Tom's own caution, so the
	   touch half is asked as a touch question -- and asked INSIDE the 640px block on purpose. Nested
	   this way the desktop above the breakpoint is provably untouched at any pointer type, including
	   a touchscreen laptop and a tablet held in landscape. What it gets wrong is the narrow window
	   on a touch tablet: it keeps the wider boxes it does not need. That is the cheap direction.

	   1.6rem, NOT the 0.2 x 3.2rem = 0.64rem Tom named. 0.64rem is 10 px, which with an input's own
	   padding and border leaves about 4 px of content -- it cannot show ONE digit, and these two
	   columns are bounded at 32 and 99, so they must show TWO. 1.6rem is the honest floor for two
	   digits and is still half the shipped width. It is also, since item 7's correction, exactly what
	   After is -- which is what Tom asked for when he said to make After like Decimal and Drop, so
	   on a touch screen the three of them are one width and only Before is wider. The floor rises to
	   9rem with them: 2.08 + three at 1.6 = 6.88rem of boxes plus the gaps and margins. */
	@media (hover: none) and (pointer: coarse) {
		html:has(#lpn_canvas) #lpn_labels_node_fields > div > :nth-child(4),
		html:has(#lpn_canvas) #lpn_labels_node_fields > div > :nth-child(5),
		html:has(#lpn_canvas) #lpn_labels_link_fields > div > :nth-child(4),
		html:has(#lpn_canvas) #lpn_labels_link_fields > div > :nth-child(5) { width: 1.6rem !important; }
		html:has(#lpn_canvas) #lpn_labels_node_fields input.ec-spin,
		html:has(#lpn_canvas) #lpn_labels_link_fields input.ec-spin { appearance: textfield; -moz-appearance: textfield; }
		html:has(#lpn_canvas) #lpn_labels_node_fields input.ec-spin::-webkit-outer-spin-button,
		html:has(#lpn_canvas) #lpn_labels_node_fields input.ec-spin::-webkit-inner-spin-button,
		html:has(#lpn_canvas) #lpn_labels_link_fields input.ec-spin::-webkit-outer-spin-button,
		html:has(#lpn_canvas) #lpn_labels_link_fields input.ec-spin::-webkit-inner-spin-button {
			appearance: none; -webkit-appearance: none; margin: 0;
		}
		html:has(#lpn_canvas) #lpn_labels_node_fields,
		html:has(#lpn_canvas) #lpn_labels_link_fields { min-width: 9rem; }

	}

	/* 5. THE TWO MAP OVERLAYS DO NOT GET TO EAT THE DRAWING (ROADMAP Task 524). Tom, 2026-08-25,
	   from a phone: the EPANET minor-loss note "is stuck open". It is not stuck -- #lpn_status is an
	   advisory overlay that stands until the next solve replaces it, which is harmless in a desktop
	   corner and is a quarter of the canvas on a phone, sitting on the network.

	   #lpn_mode_hint GOES, and it goes because on touch it is not merely large but WRONG: "Click an
	   asset... Drag to move... Double-click a pipe to add or remove a vertex" are mouse verbs, and
	   there is no double-click on a phone. Task 253's clean-map toggle already hides exactly this
	   element plus the coordinate readout, so a map without it is a state the page is known to
	   survive -- this is that state, arrived at by screen size instead of by a menu.

	   #lpn_status STAYS, because it is where a refusal to solve is announced and losing that would
	   be the worse failure. It is bounded instead:

	   - `max-width` goes UP, from the inline 60% to 92%. On a 400px phone 60% is 240px, and a
	     narrow box makes a long note TALLER -- the inline value was tuned for a desktop where 60%
	     is roomy, and it is exactly backwards here.
	   - `max-height` caps it at a third of the short side and lets it scroll, so no note of any
	     length can take more of the drawing than that. */
	html:has(#lpn_canvas) #lpn_mode_hint { display: none; }
	html:has(#lpn_canvas) #lpn_status {
		max-width: 92%;
		max-height: 33svh;
		overflow-y: auto;
	}
}

/* ---- The profile's path chooser (Tasks 433, 504) ---------------------------------------------
   The line that says what the next press does. It lives in the profile panel's control row, which
   is `font-size: .9em` already, and since Task 504 it is the ONLY thing in there that changes --
   the button that used to arm the gesture is the Profile button itself now. */
.lpn-profile-say:empty { display: none; }
.lpn-profile-say { margin: .25em 0 .4em; line-height: 1.3; }
/* The candidate route and the ring on the node under the pointer. `pointer-events: none` is already
   on the whole layer; this only exists so the dashes cannot be mistaken for the committed route by
   anyone reading the SVG, and so a future stylesheet has one name to reach for. */
.lpn-profile-ghost { pointer-events: none; }

/* ---- The profile has no side interface (Task 504) --------------------------------------------
   Tom, 2026-08-24: *"I still think we can ditch the entire side interface for the profile."* With
   the pull-downs, the chooser button and the waypoint chips gone, what stood in a 15rem column was
   four lines of TEXT holding a quarter of the pane's width away from the drawing it describes.

   So the column becomes a header ROW and the chart gets the whole width. The objection the old
   layout was built on -- that a row spends the pane's scarcest dimension, its height -- was right
   about a stack of controls and is wrong about one wrapping line of text: the title, the key and
   the commentary sit on one baseline and cost about 1.4em, where the column cost 15rem of width
   for the life of the pane.

   These rules come after the ones they replace, so the column declarations above are simply
   overridden rather than deleted -- the markup is unchanged and #lpn_profile_chart still flexes. */
.lpn-profile-panel { flex-direction: column; }
.lpn-profile-controls {
	flex: 0 0 auto;
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: .1em 1em;
	overflow: visible;
}
.lpn-profile-controls > * { margin: 0; }
/* The commentary line is the only thing left that CHANGES, so it is the one allowed to take the
   rest of the row -- and to be the item that wraps to a line of its own when the pane is narrow. */
#lpn_profile_form { flex: 1 1 16em; min-width: 12em; display: flex; flex-wrap: wrap;
	align-items: baseline; gap: .1em .6em; }
.lpn-profile-say { margin: 0; }
/* ---- The Edit door and the box behind it (Task 509) -----------------------------------------
   The panel's ONE control. It is a small button on the commentary line rather than anything larger,
   because the entry point is not the feature: the feature is the overlay it opens, and the line
   beside it still has to be the thing the eye reads first. */
.lpn-profile-edit {
	flex: 0 0 auto; font: inherit; font-size: .95em; padding: 1px 8px;
	border: 1px solid #bbb; border-radius: 3px; background: #f6f6f6; cursor: pointer;
}
.lpn-profile-edit:hover, .lpn-profile-edit:focus { background: #eaeaea; border-color: #888; }
/* **PRESSED MEANS THE PATH IS IN EDIT MODE** (Task 509) -- the drawing is now carrying handles, and
   a mode whose only evidence is on the map is a mode somebody gets stuck in. The toolbar's own
   pressed colours, not a new pair, because it is the same fact wearing the same clothes. */
.lpn-profile-edit[aria-pressed="true"] { background: #cfe3f7; border-color: #6aa6d8; }
/* The grab handles themselves. A SOLID dot is a stop the reader chose -- drag it to move it, click
   it to take it off; a HOLLOW one is a node the route merely passes through, and dragging it makes
   it a stop. Told apart by fill rather than by hue, so the distinction survives a colour-blind
   reader and a grey print. Sized in the drawing's own units by the code, which is why nothing here
   sets a radius. */
.lpn-profile-handle { pointer-events: none; }
/* A waypoint, and the Remove-all beside it. Each chip removes ITSELF -- the × is part of the label
   rather than a second control inside the chip, so the whole chip is the target a hand can hit. */
.lpn-profile-chip {
	font: inherit; font-size: .9em; margin: 0 .25em .25em 0; padding: 1px 6px;
	border: 1px solid #bbb; border-radius: 10px; background: #f6f6f6; cursor: pointer;
}
.lpn-profile-chip:hover, .lpn-profile-chip:focus { background: #eaeaea; border-color: #888; }
/* ---- The saved-path menu on the Profile tab (Task 510) ---------------------------------------
   An arrow ON the tab, not a second tab and not a toolbar button: Tom's placement, 2026-08-24, and
   it puts the list of names where the thing they name already lives.

   **IT IS THE PROJECT TAB'S CARET, NOT A THIRD TREATMENT** (Tom, 2026-08-25: *"The saved paths
   arrow is not designed right. It should be similar to the Project arrow and the Google Sheets tab
   arrow. It's too small and non-conforming to be discoverable."*). The first cut was .8em type in
   5px of padding, pulled 8px back INTO the tab's own padding -- a 17px mark that read as part of
   the word rather than as a control. .lpn-tab-caret above is this page's existing answer to
   exactly this problem on the project tabs, and it is the spreadsheet arrow Tom named, so the
   numbers here are its numbers: full type, the tab's own vertical padding, and a hairline rule
   separating it from the word instead of an overlap.

   **AND IT IS A TOUCH TARGET, which on THIS page needs saying.** The drawing surface is designed
   for a pointer and a 44px rule has no standing there; this is chrome, and a phone user has to hit
   it -- so the width floor rises to 44px below 40rem while the tabs beside it are giving padding
   up. It grows sideways rather than downward on purpose: the strip sits above a chart that has the
   least height to spare on a phone (Task 527), and a taller row would be paid for out of that. */
.lpn-pane-tab-menu {
	background: none; border: 1px solid transparent; border-bottom: 0;
	border-inline-start: 1px solid #ccc; font: inherit;
	padding: 3px 8px; margin-inline-start: -2px; min-width: 2rem;
	cursor: pointer; color: #333;
}
.lpn-pane-tab-menu:hover, .lpn-pane-tab-menu:focus { background: #ddd; }
/* Joined to the tab it belongs to, the way .lpn-tab-caret is joined to .lpn-tab-current: one
   selected tab carrying a caret, not a selected tab beside an unselected button. */
.lpn-pane-tab-menu-current { background: #fff; border-color: #bbb; border-inline-start-color: #ccc; }
@media (max-width: 40rem) {
	.lpn-pane-tab-menu { min-width: 2.75rem; padding: 3px 12px; }
}
@media (max-width: 40rem) {
	/* The stacked layout already scrolls the panel; the header row must not also become a column
	   of four lines on a phone, where the chart has the least height to spare. */
	.lpn-profile-panel { overflow: auto; }
}

/* **A LONG PRESS MUST NOT BECOME A SELECTION OR A CALLOUT** (Task 504). The waypoint press is a
   held one, and a held finger on a phone is also the browser's own gesture for "select this text"
   or "show me a menu about this". `touch-action: none` above already stops the pan/zoom half; these
   two stop the other half. On the canvas rather than on the chooser, because there is nothing on
   this drawing a visitor selects as text in any mode. */
#lpn_canvas { user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }

