Spaces:
Sleeping
Sleeping
| /* | |
| Copyright (c) 2025-2026, RTE (https://www.rte-france.com) | |
| This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. | |
| If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, | |
| you can obtain one at http://mozilla.org/MPL/2.0/. | |
| SPDX-License-Identifier: MPL-2.0 | |
| This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. | |
| */ | |
| /* ===== SVG Container ===== */ | |
| .svg-container { | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| /* The pypowsybl SVG is transparent, so the container provides the | |
| diagram backdrop. Tokenised so dark mode flips it to near-black. */ | |
| background: var(--color-diagram-surface); | |
| /* CSS containment: tells the browser this subtree's layout/paint is | |
| independent of the rest of the page, reducing work during viewBox changes */ | |
| contain: layout style paint; | |
| } | |
| /* ===== Dark-mode diagram legibility ===== | |
| pypowsybl bakes voltage-coded line colours into the SVG via an inline | |
| <style> block; those read fine on the near-black backdrop. The text | |
| does NOT. Two distinct cases: | |
| - edge-info flow values are SVG <text> (dark fill) → recolour light. | |
| - VL labels are HTML <div>s inside <foreignObject> (dark text, no | |
| backing) → give them a light text + dark chip. | |
| We deliberately do NOT blanket-recolour every <text> in the SVG: the | |
| action-overview pin labels are <text> with their own fills (dark text | |
| on a white pin glyph) and must stay dark. So edge-info is targeted by | |
| class. `!important` beats pypowsybl's appended inline <style>. */ | |
| [data-theme="dark"] .svg-container .nad-edge-infos text { | |
| fill: var(--color-text-primary) ; | |
| /* pypowsybl paints a white stroke "halo" under each flow value so it | |
| reads over the coloured line. On the near-black backdrop that white | |
| halo turns the light text fuzzy — repaint the halo in the backdrop | |
| colour (still legible over lines, no white fringe). */ | |
| stroke: var(--color-diagram-surface) ; | |
| paint-order: stroke fill ; | |
| } | |
| [data-theme="dark"] .svg-container foreignObject.nad-text-nodes div, | |
| [data-theme="dark"] .svg-container .nad-vl-nodes foreignObject div, | |
| [data-theme="dark"] .svg-container .nad-label-nodes foreignObject div, | |
| [data-theme="dark"] .svg-container .nad-label-box { | |
| color: var(--color-text-primary) ; | |
| background-color: rgba(27, 31, 36, 0.85) ; | |
| border-color: var(--color-border) ; | |
| } | |
| /* Action-overview "dim the network" rect — set to fill='white' inline | |
| for light mode. Drive its colour from the diagram-surface token (a | |
| CSS `fill` property beats the inline presentation attribute), so in | |
| dark mode it dims toward near-black instead of leaving a grey square | |
| over the dark backdrop. */ | |
| .svg-container .nad-overview-dim-rect { | |
| fill: var(--color-diagram-surface); | |
| } | |
| /* ===== Dark-mode SLD legibility ===== | |
| The Single-Line-Diagram overlay injects pypowsybl `sld-*` markup onto | |
| a dark panel. Feeder / power-value labels (`.sld-label`) and graph | |
| labels are dark <text> → recolour them to light. We exclude the | |
| flow-delta text classes (`sld-delta-text-*`) because those carry a | |
| meaningful positive / negative / neutral colour applied by | |
| SldOverlay. Scoped to the overlay root so nothing else is touched. */ | |
| [data-theme="dark"] [data-testid="sld-overlay"] text:not([class*="sld-delta-text"]) { | |
| fill: var(--color-text-primary) ; | |
| } | |
| .svg-container svg { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Keep strokes at constant screen-pixel width regardless of viewBox zoom. | |
| Without this, lines become invisible at full zoom-out and extremely thick | |
| when zoomed in, causing both visual loss of pypowsybl native colors and | |
| expensive rendering / zoom lag on large grids. */ | |
| .svg-container svg path, | |
| .svg-container svg line, | |
| .svg-container svg polyline, | |
| .svg-container svg rect { | |
| vector-effect: non-scaling-stroke; | |
| } | |
| /* During active pan/zoom interaction disable pointer-events on all SVG | |
| children to skip expensive hit-testing during drag/zoom. */ | |
| .svg-container.svg-interacting svg * { | |
| pointer-events: none ; | |
| } | |
| /* ===== Interaction-time paint culling ===== | |
| On a large NAD (≈5k voltage levels / ≈100k DOM nodes) the per-frame | |
| cost of a viewBox pan/zoom is dominated by two element classes the | |
| browser must re-rasterise every frame: the HTML voltage-level labels | |
| (<foreignObject> — by far the most expensive SVG primitive to paint) | |
| and the edge-info flow values + direction arrows. They are unreadable | |
| mid-gesture anyway, so we drop them from the render tree while a | |
| gesture is active (`usePanZoom` toggles `.svg-interacting` on | |
| wheel/drag start and clears it on settle). On settle they repaint | |
| once at the resting viewBox. | |
| This is GPU-independent — it shrinks the work of the plain viewBox | |
| repaint path itself, so it holds up even on software-rendered / | |
| remote-desktop / GPU-blocklisted sessions (common for control-room | |
| operators) where a CSS-transform compositing trick would instead | |
| regress badly (every pan frame re-rasters new tiles of the whole | |
| layer). Measured on the pypsa_eur_eur220_225_380_400 grid (5247 VLs, | |
| 8205 branches, 7.3 MB SVG) with an interleaved A/B headless benchmark, | |
| per painted frame: pan 290 ms -> 188 ms (~1.5x), zoom 378 ms -> 226 ms | |
| (~1.7x). The win is dropping the <foreignObject> HTML labels (the most | |
| expensive SVG paint primitive) and is expected to be larger on GPU | |
| hardware where that CPU-side raster dominates relatively more. | |
| See docs/performance/history/interaction-paint-culling.md. The selector | |
| list mirrors `.nad-hide-vl-labels` below (every shape pypowsybl uses | |
| for a VL label) plus the edge-info group. */ | |
| .svg-container.svg-interacting .nad-edge-infos, | |
| .svg-container.svg-interacting .nad-text-nodes, | |
| .svg-container.svg-interacting foreignObject.nad-text-nodes, | |
| .svg-container.svg-interacting .nad-vl-nodes foreignObject, | |
| .svg-container.svg-interacting .nad-label-nodes foreignObject, | |
| .svg-container.svg-interacting .nad-label-nodes, | |
| .svg-container.svg-interacting .nad-label-box, | |
| .svg-container.svg-interacting .nad-text-edges { | |
| display: none ; | |
| } | |
| /* ===== Zoom-tier Level-of-Detail ===== | |
| data-zoom-tier is set by usePanZoom based on the ratio of the current | |
| viewBox width to the original (full-extent) viewBox width. | |
| - "overview" (ratio > 0.5): hide text-heavy elements to reduce paint cost | |
| - "region" (0.15 < ratio ≤ 0.5): show edge info, keep minor labels hidden | |
| - "detail" (ratio ≤ 0.15): everything visible (default rendering) | |
| Elements are hidden via display:none, which removes them from the render | |
| tree without removing them from the DOM — preserving metadata, ID maps, | |
| and highlight state. */ | |
| /* Overview: hide edge-info flow values and node label boxes */ | |
| [data-zoom-tier="overview"] .nad-edge-infos { | |
| display: none; | |
| } | |
| [data-zoom-tier="overview"] .nad-vl-nodes foreignObject { | |
| display: none; | |
| } | |
| /* Region: edge-info visible, but node label boxes still hidden */ | |
| [data-zoom-tier="region"] .nad-vl-nodes foreignObject { | |
| display: none; | |
| } | |
| /* User-toggle: hide every voltage-level label on the diagram. pypowsybl | |
| emits VL labels in two interchangeable shapes depending on the diagram | |
| configuration: a root-level `<foreignObject class="nad-text-nodes">` | |
| carrying HTML `<div>` labels, and inline `<text>` siblings under | |
| `.nad-vl-nodes` / `.nad-label-nodes`. Cover both shapes (and the | |
| inline `nad-label-box` divs that some pypowsybl builds emit at the | |
| SVG root without a foreignObject wrapper) so the toggle works across | |
| all networks. `.nad-text-edges` hides the connector tick that links | |
| each voltage-level node to its label box — without it the label is | |
| gone but a dangling line remains. `!important` overrides pypowsybl's | |
| own inline `<style>` block, which is appended to the document AFTER | |
| App.css when the SVG is injected by `replaceChildren(svg)`. */ | |
| .svg-container.nad-hide-vl-labels .nad-text-nodes, | |
| .svg-container.nad-hide-vl-labels foreignObject.nad-text-nodes, | |
| .svg-container.nad-hide-vl-labels .nad-vl-nodes foreignObject, | |
| .svg-container.nad-hide-vl-labels .nad-label-nodes foreignObject, | |
| .svg-container.nad-hide-vl-labels .nad-label-nodes, | |
| .svg-container.nad-hide-vl-labels .nad-label-box, | |
| .svg-container.nad-hide-vl-labels .nad-text-edges { | |
| display: none ; | |
| } | |
| /* Voltage-level disks are interactive: single-click selects (Inspect + | |
| auto-zoom), double-click opens the SLD, hover shows the name while the | |
| static labels are hidden (see utils/svg/vlInteractions.ts). The VL name | |
| box is interactive too — a single click opens the SLD. The pointer | |
| cursor is a static rule so the affordance costs nothing at render or | |
| gesture time; during a pan/zoom `.svg-interacting` disables SVG hit- | |
| testing, so the container's grab/grabbing cursor wins instead. */ | |
| .svg-container .nad-vl-nodes, | |
| .svg-container .nad-label-box { | |
| cursor: pointer; | |
| } | |
| /* ===== Highlight styles ===== */ | |
| .nad-highlight { | |
| stroke: var(--color-danger) ; | |
| stroke-width: 4px ; | |
| opacity: 1; | |
| } | |
| circle.nad-highlight { | |
| fill: var(--color-danger) ; | |
| } | |
| /* Highlight overloaded lines in orange (clone-based halo) */ | |
| .nad-overloaded { | |
| opacity: 1 ; | |
| } | |
| .nad-overloaded path, | |
| .nad-overloaded line, | |
| .nad-overloaded polyline, | |
| .nad-overloaded rect, | |
| .nad-overloaded polygon { | |
| stroke: var(--signal-overload) ; | |
| stroke-width: 120px ; | |
| stroke-opacity: 1 ; | |
| stroke-dasharray: none ; | |
| fill: none ; | |
| stroke-linecap: round ; | |
| stroke-linejoin: round ; | |
| } | |
| .nad-overloaded text, | |
| .nad-overloaded foreignObject { | |
| display: none ; | |
| } | |
| /* | |
| * Disconnected branch marker used by the svgPatch DOM-recycling path | |
| * (see docs/performance/history/svg-dom-recycling.md). Applied to | |
| * edges whose equipment ID is listed in the patch payload's | |
| * `disconnected_edges` — the contingency line on the N-1 tab, plus | |
| * any action-induced extras. Mirrors pypowsybl's native | |
| * disconnected-branch look so a patched N-1 reads the same as a | |
| * full-fetch N-1. `vector-effect: non-scaling-stroke` keeps the | |
| * dash length stable at zoom-out on large grids (same rationale as | |
| * the guard in docs/performance/rendering-optimization-plan.md). | |
| */ | |
| .nad-disconnected, | |
| .nad-disconnected > g, | |
| .nad-disconnected path, | |
| .nad-disconnected line, | |
| .nad-disconnected polyline, | |
| .nad-disconnected > g path, | |
| .nad-disconnected > g line, | |
| .nad-disconnected > g polyline { | |
| stroke-dasharray: 20 10 ; | |
| stroke-opacity: 1 ; | |
| vector-effect: non-scaling-stroke ; | |
| } | |
| /* Remedial action targets: purple-pink */ | |
| .nad-action-target { | |
| opacity: 1 ; | |
| } | |
| /* Contingency highlight: yellow */ | |
| .nad-contingency-highlight { | |
| opacity: 1 ; | |
| } | |
| /* For lines and paths, use grid units for stroke-width so the halo is always | |
| significantly wider than the actual element it highlights. */ | |
| .nad-action-target path, | |
| .nad-action-target line, | |
| .nad-action-target polyline, | |
| .nad-action-target rect, | |
| .nad-action-target polygon { | |
| stroke: var(--signal-action-target) ; | |
| stroke-width: 150px ; | |
| stroke-opacity: 1 ; | |
| stroke-dasharray: none ; | |
| fill: none ; | |
| stroke-linecap: round ; | |
| stroke-linejoin: round ; | |
| } | |
| .nad-contingency-highlight path, | |
| .nad-contingency-highlight line, | |
| .nad-contingency-highlight polyline, | |
| .nad-contingency-highlight rect, | |
| .nad-contingency-highlight polygon { | |
| stroke: var(--signal-contingency) ; | |
| stroke-width: 150px ; | |
| stroke-opacity: 1 ; | |
| stroke-dasharray: none ; | |
| fill: none ; | |
| stroke-linecap: round ; | |
| stroke-linejoin: round ; | |
| } | |
| /* ===== Zoom-adaptive halo width (smooth, no tier snap) ===== | |
| The overload / action / contingency line halos are screen-space | |
| (`vector-effect: non-scaling-stroke`), so their `stroke-width` is rendered | |
| pixels. usePanZoom drives `--nad-halo-w` CONTINUOUSLY from the zoom ratio: | |
| - zoomed IN → thin (HALO_MIN_PX, ~24px): a clean trace of the branch, the | |
| look the operator wants at deep zoom; | |
| - zoomed OUT → grows toward a prominent marker (HALO_MAX_PX) so the | |
| contingency stays visible on the full-extent view, | |
| with no abrupt jump at a tier boundary (the old `data-zoom-tier` 24px-vs- | |
| 120px snap). This rule comes AFTER the per-class colour rules above, so the | |
| var overrides their base `stroke-width`. It defaults thin if JS hasn't set | |
| the var yet. `non-scaling-stroke` is re-asserted here as a guard against a | |
| pypowsybl build flipping equipment paths back to `vector-effect: none`. */ | |
| .nad-overloaded path, .nad-overloaded line, .nad-overloaded polyline, .nad-overloaded rect, .nad-overloaded polygon, | |
| .nad-action-target path, .nad-action-target line, .nad-action-target polyline, .nad-action-target rect, .nad-action-target polygon, | |
| .nad-contingency-highlight path, .nad-contingency-highlight line, .nad-contingency-highlight polyline, .nad-contingency-highlight rect, .nad-contingency-highlight polygon { | |
| stroke-width: var(--nad-halo-w, 24px) ; | |
| vector-effect: non-scaling-stroke ; | |
| } | |
| /* For nodes (circles and rects), add a screen-space stroke to create a glowing | |
| background halo that doesn't shrink into invisibility on large grids. */ | |
| .nad-action-target circle, | |
| .nad-action-target rect { | |
| stroke: var(--signal-action-target) ; | |
| stroke-width: 45px ; | |
| stroke-opacity: 1 ; | |
| fill: var(--signal-action-target) ; | |
| fill-opacity: 1 ; | |
| vector-effect: non-scaling-stroke ; | |
| } | |
| .nad-contingency-highlight circle, | |
| .nad-contingency-highlight rect { | |
| stroke: var(--signal-contingency) ; | |
| stroke-width: 45px ; | |
| stroke-opacity: 1 ; | |
| fill: var(--signal-contingency) ; | |
| fill-opacity: 1 ; | |
| opacity: 1 ; | |
| vector-effect: non-scaling-stroke ; | |
| } | |
| .nad-highlight-clone { | |
| opacity: 1 ; | |
| } | |
| /* Force full opacity on original assets that are currently being highlighted | |
| as action targets. This prevents them from being dimmed by Impacts mode | |
| logic (which might consider them "grey" or non-impacted), which would | |
| otherwise allow the background halo to show through and make them look | |
| transparent. */ | |
| .nad-action-target-original, | |
| .sld-highlight-action-original, | |
| .sld-highlight-breaker-original, | |
| .sld-highlight-contingency-original, | |
| .sld-highlight-overloaded-original { | |
| opacity: 1 ; | |
| stroke-opacity: 1 ; | |
| fill-opacity: 1 ; | |
| } | |
| /* Hide text and foreignObjects in the background clone to avoid messy overlays */ | |
| .nad-action-target text, | |
| .nad-action-target foreignObject { | |
| display: none ; | |
| } | |
| /* Suppress expensive drop-shadow filters during active interaction. | |
| The compositing cost of multiple drop-shadows on cloned highlight | |
| elements is high — skip it while the viewBox is changing rapidly. */ | |
| .svg-container.svg-interacting .nad-action-target, | |
| .svg-container.svg-interacting .nad-contingency-highlight, | |
| .svg-container.svg-interacting .nad-overloaded { | |
| filter: none ; | |
| } | |
| /* ===== Delta flow visualization ===== */ | |
| /* Positive delta: flow increased after action (orange) */ | |
| .nad-delta-positive path, | |
| .nad-delta-positive line, | |
| .nad-delta-positive polyline { | |
| stroke: var(--signal-delta-positive) ; | |
| stroke-width: 3px ; | |
| } | |
| /* Negative delta: flow decreased after action (blue) */ | |
| .nad-delta-negative path, | |
| .nad-delta-negative line, | |
| .nad-delta-negative polyline { | |
| stroke: var(--signal-delta-negative) ; | |
| stroke-width: 3px ; | |
| } | |
| /* Grey: negligible change (below threshold) */ | |
| .nad-delta-grey path, | |
| .nad-delta-grey line, | |
| .nad-delta-grey polyline { | |
| stroke: var(--signal-delta-neutral) ; | |
| stroke-opacity: 0.4 ; | |
| } | |
| /* ===== SLD overlay delta flow visualization ===== */ | |
| /* Applied to feeder group elements (sld-top-feeder / sld-bottom-feeder) inside | |
| the Single Line Diagram overlay when in Impacts mode. Cascades into child | |
| wires, arrows, symbols. */ | |
| .sld-delta-positive path, | |
| .sld-delta-positive line, | |
| .sld-delta-positive polyline, | |
| .sld-delta-positive rect, | |
| .sld-delta-positive circle { | |
| stroke: var(--signal-overload) ; | |
| stroke-width: 2px ; | |
| } | |
| /* Target-topology preview: flow values + arrows are stale (no load | |
| flow was run) so they're greyed until the operator re-simulates. | |
| pypowsybl renders feeder flow info as <text> and ARROW_* glyphs. */ | |
| .sld-preview-stale text { | |
| fill: var(--signal-delta-neutral) ; | |
| opacity: 0.55 ; | |
| } | |
| .sld-preview-stale .sld-feeder-info, | |
| .sld-preview-stale [class*="ARROW"] { | |
| opacity: 0.4 ; | |
| } | |
| /* Editable-element affordance while editing the SLD: a plain pointer | |
| cursor on the cells (no recolouring of branches / busbars). Loads and | |
| generators ALSO get their NAME rendered as a dark-blue button (see | |
| .sld-injection-name-btn below) to invite a click. */ | |
| .sld-switch-editable, | |
| .sld-injection-editable { | |
| cursor: pointer; | |
| } | |
| /* Relabelled branch feeder names are clickable: a click navigates the SLD | |
| overlay to that voltage level (the branch's other extremity). The dashed | |
| underline + pointer cursor signal the affordance without recolouring. */ | |
| .sld-feeder-navigable { | |
| cursor: pointer; | |
| text-decoration: underline; | |
| text-decoration-style: dotted; | |
| } | |
| .sld-feeder-navigable:hover { | |
| fill: var(--color-brand-strong) ; | |
| } | |
| /* Injection name rendered as a button (same dark blue + white text as the | |
| former "Manual action" button): a rounded rect injected behind the name | |
| <text>, with the label recoloured on top. */ | |
| .sld-injection-name-btn { | |
| fill: var(--color-brand); | |
| cursor: pointer; | |
| } | |
| .sld-injection-name-btn:hover { | |
| fill: var(--color-brand-strong); | |
| } | |
| .sld-injection-name-label { | |
| fill: var(--color-text-on-brand) ; | |
| font-weight: 600; | |
| cursor: pointer; | |
| } | |
| /* Pending switch toggles (interactive SLD topology edit). */ | |
| .sld-user-toggle-open path, | |
| .sld-user-toggle-open line, | |
| .sld-user-toggle-open polyline, | |
| .sld-user-toggle-open rect, | |
| .sld-user-toggle-open circle { | |
| stroke: var(--signal-edit-open) ; | |
| stroke-width: 2.5px ; | |
| stroke-dasharray: 4 2 ; | |
| } | |
| .sld-user-toggle-closed path, | |
| .sld-user-toggle-closed line, | |
| .sld-user-toggle-closed polyline, | |
| .sld-user-toggle-closed rect, | |
| .sld-user-toggle-closed circle { | |
| stroke: var(--signal-edit-closed) ; | |
| stroke-width: 2.5px ; | |
| stroke-dasharray: 4 2 ; | |
| } | |
| /* Pending injection (load / generator active-power) retune. */ | |
| .sld-user-injection path, | |
| .sld-user-injection line, | |
| .sld-user-injection polyline, | |
| .sld-user-injection rect, | |
| .sld-user-injection circle { | |
| stroke: var(--signal-edit-injection) ; | |
| stroke-width: 2.5px ; | |
| stroke-dasharray: 4 2 ; | |
| } | |
| .sld-delta-negative path, | |
| .sld-delta-negative line, | |
| .sld-delta-negative polyline, | |
| .sld-delta-negative rect, | |
| .sld-delta-negative circle { | |
| stroke: var(--signal-delta-negative) ; | |
| stroke-width: 2px ; | |
| } | |
| .sld-delta-grey path, | |
| .sld-delta-grey line, | |
| .sld-delta-grey polyline, | |
| .sld-delta-grey rect, | |
| .sld-delta-grey circle { | |
| stroke: var(--signal-delta-neutral) ; | |
| stroke-opacity: 0.5 ; | |
| } | |
| /* Delta text label styling */ | |
| .sld-delta-text-positive { | |
| fill: var(--signal-overload) ; | |
| } | |
| .sld-delta-text-negative { | |
| fill: var(--signal-delta-negative) ; | |
| } | |
| .sld-delta-text-grey { | |
| fill: var(--signal-delta-neutral) ; | |
| fill-opacity: 0.5 ; | |
| } | |
| /* ===== SLD impact highlights ===== */ | |
| /* These classes are applied to CLONED elements placed in a background layer, | |
| so they don't affect the original SLD rendering (flows/impacts modes). */ | |
| /* Contingency element on N-1 SLD (yellow glow behind) */ | |
| .sld-highlight-clone.sld-highlight-contingency { | |
| filter: drop-shadow(0 0 4px var(--signal-contingency)) drop-shadow(0 0 8px var(--signal-contingency)); | |
| } | |
| .sld-highlight-clone.sld-highlight-contingency path, | |
| .sld-highlight-clone.sld-highlight-contingency line, | |
| .sld-highlight-clone.sld-highlight-contingency polyline, | |
| .sld-highlight-clone.sld-highlight-contingency rect, | |
| .sld-highlight-clone.sld-highlight-contingency circle { | |
| stroke: var(--signal-contingency) ; | |
| stroke-width: 6px ; | |
| stroke-opacity: 1 ; | |
| stroke-dasharray: none ; | |
| fill: none ; | |
| } | |
| /* Action target on action SLD (purple-pink halo behind) */ | |
| .sld-highlight-clone.sld-highlight-action { | |
| filter: drop-shadow(0 0 4px var(--signal-action-target)) drop-shadow(0 0 8px var(--signal-action-target)); | |
| } | |
| .sld-highlight-clone.sld-highlight-action path, | |
| .sld-highlight-clone.sld-highlight-action line, | |
| .sld-highlight-clone.sld-highlight-action polyline, | |
| .sld-highlight-clone.sld-highlight-action rect, | |
| .sld-highlight-clone.sld-highlight-action circle { | |
| stroke: var(--signal-action-target) ; | |
| stroke-width: 6px ; | |
| stroke-opacity: 1 ; | |
| fill: none ; | |
| } | |
| /* Overloaded lines on SLD (orange halo, consistent with contingency style) */ | |
| .sld-highlight-clone.sld-highlight-overloaded { | |
| filter: drop-shadow(0 0 4px var(--signal-overload)) drop-shadow(0 0 8px var(--signal-overload)); | |
| } | |
| .sld-highlight-clone.sld-highlight-overloaded path, | |
| .sld-highlight-clone.sld-highlight-overloaded line, | |
| .sld-highlight-clone.sld-highlight-overloaded polyline, | |
| .sld-highlight-clone.sld-highlight-overloaded rect, | |
| .sld-highlight-clone.sld-highlight-overloaded circle { | |
| stroke: var(--signal-overload) ; | |
| stroke-width: 6px ; | |
| stroke-opacity: 1 ; | |
| stroke-dasharray: none ; | |
| fill: none ; | |
| } | |
| /* Affected breakers/switches on SLD (light purple halo behind) */ | |
| .sld-highlight-clone.sld-highlight-breaker { | |
| filter: drop-shadow(0 0 3px var(--signal-breaker)) drop-shadow(0 0 6px var(--signal-breaker)); | |
| } | |
| .sld-highlight-clone.sld-highlight-breaker path, | |
| .sld-highlight-clone.sld-highlight-breaker line, | |
| .sld-highlight-clone.sld-highlight-breaker rect, | |
| .sld-highlight-clone.sld-highlight-breaker circle { | |
| stroke: var(--signal-breaker) ; | |
| stroke-width: 6px ; | |
| stroke-opacity: 1 ; | |
| fill: none ; | |
| } | |
| /* Hide text/labels in cloned highlight elements */ | |
| .sld-highlight-clone text, | |
| .sld-highlight-clone .sld-label, | |
| .sld-highlight-clone foreignObject { | |
| display: none ; | |
| } | |
| /* ===== Voltage Range Slider (vertical, right sidebar) ===== */ | |
| .voltage-sidebar { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| bottom: 0; | |
| width: 62px; | |
| background: var(--color-surface-muted); | |
| border-left: 1px solid var(--color-border); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| z-index: 15; | |
| padding: var(--space-2) 0; | |
| box-sizing: border-box; | |
| } | |
| .voltage-sidebar .vs-label { | |
| font-size: 0.65rem; | |
| font-weight: bold; | |
| color: var(--color-chrome); | |
| writing-mode: vertical-rl; | |
| text-orientation: mixed; | |
| letter-spacing: 0.5px; | |
| margin-bottom: var(--space-1); | |
| } | |
| .voltage-sidebar .vs-range-label { | |
| font-size: 0.6rem; | |
| font-weight: bold; | |
| color: var(--color-chrome); | |
| background: var(--color-brand-soft); | |
| border-radius: 3px; | |
| padding: var(--space-half) var(--space-1); | |
| margin-bottom: var(--space-2); | |
| text-align: center; | |
| line-height: 1.2; | |
| } | |
| .voltage-slider-container { | |
| position: relative; | |
| flex: 1; | |
| width: 28px; | |
| min-height: 0; | |
| } | |
| .voltage-slider-track { | |
| position: absolute; | |
| left: 12px; | |
| top: 0; | |
| bottom: 0; | |
| width: 4px; | |
| background: var(--color-border); | |
| border-radius: 2px; | |
| } | |
| .voltage-slider-range { | |
| position: absolute; | |
| left: 12px; | |
| width: 4px; | |
| background: var(--color-brand-mid); | |
| border-radius: 2px; | |
| } | |
| .voltage-slider-container input[type=range] { | |
| position: absolute; | |
| left: 0; | |
| width: 28px; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| background: transparent; | |
| pointer-events: none; | |
| margin: 0; | |
| writing-mode: vertical-lr; | |
| direction: rtl; | |
| } | |
| /* Suppress the Chrome / Safari spinner arrows on the compact | |
| * Max-loading threshold input inside the ActionFilterRings — the bar | |
| * needs to fit on a single row so the extra arrow column is dropped. | |
| * Firefox is covered by the inline ``MozAppearance: textfield`` style. | |
| */ | |
| input[data-testid="sidebar-filter-threshold-input"]::-webkit-inner-spin-button, | |
| input[data-testid="sidebar-filter-threshold-input"]::-webkit-outer-spin-button { | |
| -webkit-appearance: none; | |
| margin: 0; | |
| } | |
| .voltage-slider-container input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 18px; | |
| width: 18px; | |
| border-radius: 50%; | |
| background: var(--color-brand-mid); | |
| border: 2px solid white; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); | |
| cursor: pointer; | |
| pointer-events: all; | |
| } | |
| .voltage-slider-container input[type=range]::-moz-range-thumb { | |
| height: 18px; | |
| width: 18px; | |
| border-radius: 50%; | |
| background: var(--color-brand-mid); | |
| border: 2px solid white; | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); | |
| cursor: pointer; | |
| pointer-events: all; | |
| } | |
| .voltage-slider-ticks { | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| left: 28px; | |
| width: 24px; | |
| } | |
| .voltage-slider-ticks span { | |
| position: absolute; | |
| font-size: 0.58rem; | |
| color: var(--color-text-tertiary); | |
| white-space: nowrap; | |
| transform: translateY(-50%); | |
| left: 2px; | |
| } | |
| /* Collapsed voltage filter toggle button */ | |
| .voltage-sidebar-toggle { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| bottom: 0; | |
| width: 24px; | |
| background: var(--color-surface-muted); | |
| border: none; | |
| border-left: 1px solid var(--color-border); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 15; | |
| color: var(--color-text-tertiary); | |
| font-size: var(--text-xs); | |
| padding: 0; | |
| } | |
| .voltage-sidebar-toggle:hover { | |
| background: var(--color-surface-raised); | |
| color: var(--color-text-primary); | |
| } | |
| /* ============================================================ | |
| ActionCard progressive disclosure | |
| ============================================================ | |
| The star (⭐) and reject (✕) controls live in a "rail" in | |
| the top-right of the card header (next to the severity icon | |
| + title row). The rail fades in on hover, on focus, and | |
| whenever the card is the currently-viewed action. This trims | |
| the at-rest visual load while keeping the controls a single | |
| mouse-move away. | |
| Implemented in CSS (not React state) so hover doesn't trigger | |
| a re-render on every card. */ | |
| .action-card-rail { | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.12s ease; | |
| } | |
| .action-card:hover .action-card-rail, | |
| .action-card:focus-within .action-card-rail, | |
| .action-card.is-viewing .action-card-rail { | |
| opacity: 1; | |
| pointer-events: auto; | |
| } | |
| /* ============================================================ | |
| Action-overview pins (Remedial Action tab, no card selected) | |
| ============================================================ */ | |
| .nad-action-overview-pin { | |
| transition: transform 0.15s ease; | |
| transform-box: fill-box; | |
| transform-origin: center bottom; | |
| } | |
| .nad-action-overview-pin:hover { | |
| filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.35)); | |
| } | |
| .nad-action-overview-pin:hover > path { | |
| stroke-width: 2.5; | |
| } | |
| /* The dim effect is achieved by a single white <rect> with | |
| opacity 0.65 inserted ABOVE the NAD content. Highlights are | |
| inserted at the START of the SVG (behind NAD content), matching | |
| the N-1 tab's background-layer pattern. Pins sit on top. | |
| Visual stack: highlights → NAD content → dim rect → pins. | |
| No per-child opacity or stacking contexts needed. | |
| See ActionOverviewDiagram.tsx preparedSvg. */ | |
| .nad-action-overview-container svg { | |
| display: block; | |
| /* Trade anti-aliasing quality for rendering speed. On a | |
| 10k+ element power-grid NAD the difference can be several | |
| seconds of paint time. */ | |
| shape-rendering: optimizeSpeed; | |
| text-rendering: optimizeSpeed; | |
| } | |