/* ---------------------------------------------------------------------------------------------
   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; }

.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;
}

/* 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;
}

/* 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. */
#lpn_canvas { touch-action: none; cursor: default; --lpn-sym: 1; --lpn-opacity: 1; --lpn-backdrop-opacity: 1; --lpn-map-bg: #fff; }
/* 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. */.lpn-node { fill: #000; stroke: none; 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:all keeps its full disc clickable/draggable
   anyway (data-node, drag, hit-testing all still read THIS element, unchanged), same trick used
   for any invisible-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. */
.lpn-node-reservoir, .lpn-node-tank { fill: none; stroke: none; pointer-events: all; }
/* 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. */
.lpn-node-symbol-reservoir, .lpn-node-symbol-tank { color: #000; }
/* 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: #000; }
/* 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: #000; }
/* 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. */
.lpn-node-symbol-backdrop, .lpn-link-symbol-backdrop { fill: var(--lpn-map-bg, #fff); stroke: none; }
/* 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: #000; 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: #000; }
/* 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. */
.lpn-vhandle { fill: #000; stroke: #223; stroke-width: calc(0.25 * var(--lpn-sym, 1)); cursor: move; }
/* ---- 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. visibility:hidden also drops
   pointer events, so nothing suppressed here is still clickable.

   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; }
.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; }
}
@media (prefers-reduced-motion: reduce) {
	.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; } }
.lpn-draglbl { cursor: move; pointer-events: all; }
/* 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. */
.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; }
/* A leading checkmark, not just bold/outline (Tom) -- reads as a standard menu "this is
   active" mark rather than a generic emphasis style. */
#lpn_toolbar button[aria-pressed="true"]::before { content: "\2713 "; }
/* Three groups (Add / Delete-Undo-Select / Zoom-Example) -- a wider gap, not a line, separates
   them (Tom, 2026-07-30: a divider line was more than needed). */
#lpn_toolbar { display: flex; flex-wrap: wrap; align-items: center; row-gap: 4px; }
.lpn-toolbar-group { display: inline-flex; gap: 6px; margin-right: 14px; }
/* 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. */
#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;
}
.lpn-tabs-scroll { display: flex; align-items: flex-end; gap: 2px; overflow-x: auto; 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); }
/* The property popup is DRAGGABLE by its chrome -- the padded band around the body, which is the
   only part of it where the event target is the popup element itself (see wirePopup()). The cursor
   says so there, and the body puts it back, so a text input never shows a move cursor. Only
   #lpn_popup: the Labels and Settings pull-downs share .lpn-popover but hang off toolbar buttons
   and are dismissed by clicking away, so dragging one would be a gesture with nothing to return
   it. `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. */
#lpn_popup { cursor: move; touch-action: none; }
#lpn_popup .lpn-popover-body { cursor: 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, in one declaration. Two honest products: a DRAWING (dark linework and
   labels -- what you plot) and a THEMATIC MAP (colour by one field, no labels -- what you read at
   a glance across 97 nodes). EPANET's mistake is not having colour, it is having only the second
   and making the first hard, so this is a MODE and never the default.
   The labels are hidden by a CLASS ON THE SVG rather than by switching labelSettings off. That
   distinction is the whole reason the mode is reversible: after switching the mode off, the user
   gets back exactly the labels they had, and nothing they see afterwards is a state this code
   wrote into their project. Flow arrows deliberately stay -- direction is not a number, and it is
   the one annotation you still need at a glance. */
.lpn-thematic .lpn-lbl,
.lpn-thematic .lpn-leader { display: none; }

/* 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. */
.lpn-units-group { display: block; }
.lpn-units-head { display: inline-block; min-width: 11em; font-weight: 600; opacity: .8; }

/* ==== 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. Its viewBox does the fitting (preserveAspectRatio's default keeps the
   drawing whole), so a taller pane is a bigger chart with no second layout to keep in step -- which
   is the answer to Tom's "it is too small" (Task 433). The 36rem cap it wore as a popover is gone
   with the popover. */
.lpn-profile-svg { 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-chip { margin: 0 .25em .15em 0; }
.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; }
.lpn-pane-head { display: flex; align-items: stretch; gap: 4px; border-bottom: 1px solid #ddd; }
.lpn-pane-tabs { display: flex; flex-wrap: wrap; gap: 2px; flex: 1 1 auto; min-width: 0; }
/* 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; }
.lpn-pane-tab[aria-selected="true"] { background: #fff; border-color: #bbb; font-weight: bold; }
.lpn-pane-x {
	flex: 0 0 auto; width: 2.2rem; 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; }
#lpn_profile_chart { flex: 1 1 auto; min-width: 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; }
/* The tabular editors. Column width is king: headings stay narrow, cells stay tight, and the
   number columns are right-aligned so a column of figures reads as one. 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; }
.lpn-pane-table th, .lpn-pane-table td { padding: 1px 6px 1px 0; text-align: left; border: 0; }
.lpn-pane-table thead th { position: sticky; top: 0; background: #fff; border-bottom: 1px solid #ccc; }
.lpn-pane-table td.lpn-pane-num { text-align: right; font-variant-numeric: tabular-nums; }
.lpn-pane-table input { width: 7em; }
/* 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;
}
.lpn-pane-sort { font-weight: bold; }
.lpn-pane-goto { text-decoration: underline; }
.lpn-pane-sort:hover, .lpn-pane-goto:hover { color: #0645ad; }

/* ==== 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 `3.5em`/`4.5em` 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, Decimals, 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 4.5rem 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: 3.5rem !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: 4.5rem !important; }

/* ==== 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; }
