:root {
	--bg: #121212;
	--panel: #1e1e1e;
	--topbar: #191919;
	--fg: #eaeaea;
	--accent: #d50000;
	--grid-line: rgba(190, 190, 190, 0.16);
	--grid-size: 0.75in;
}

:root[data-theme="light"] {
	--bg: #f2f2f2;
	--panel: #ffffff;
	--topbar: #ffffff;
	--fg: #1a1a1a;
	--grid-line: rgba(0, 0, 0, 0.08);
}

* {
	box-sizing: border-box;
}

.hidden {
	display: none !important;
}

body {
	margin: 0;
	min-height: 100vh;
	background-color: var(--bg);
	background-image:
		linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
		linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
	background-size: var(--grid-size) var(--grid-size);
	color: var(--fg);
	font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

.topbar {
	display: grid;
	grid-template-columns: 1fr auto 1fr;
	align-items: center;
	padding: 0.75rem 1.25rem;
	background: var(--topbar);
	border-bottom: 1px solid var(--accent);
}

.brand {
	display: flex;
	align-items: center;
	font-size: 1.5rem;
	font-weight: 700;
	color: var(--accent);
	text-decoration: none;
	justify-self: start;
}

/* Optional per-page content (e.g. field.php's title + datapoint summary),
   supplied via $topbarCenterHtml. Truly centered on the full topbar width
   regardless of how wide the brand/nav columns are, since it's its own
   grid column flanked by two equal 1fr columns. Empty on every page that
   doesn't set $topbarCenterHtml. */
.topbar-center {
	display: flex;
	align-items: center;
	gap: 0.9rem;
	justify-self: center;
	min-width: 0;
}

.topbar-field-title {
	color: var(--fg);
	font-weight: 600;
	font-size: 1.05rem;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	max-width: 40vw;
}

.topbar-nav {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	justify-self: end;
}

.brand-logo {
	display: inline-block;
	height: 1.75rem;
	width: 1.75rem;
	margin-right: 0.5rem;
	background-image: url("data:image/png;base64,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");
	background-size: contain;
	background-repeat: no-repeat;
	background-position: center;
}

.tm {
	font-size: 0.55em;
	vertical-align: super;
	margin-left: 1px;
}

.icon-btn {
	margin-left: auto;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	background: none;
	border: 1px solid var(--accent);
	border-radius: 50%;
	color: var(--fg);
	font-size: 1.3rem;
	line-height: 1;
	padding: 0;
	cursor: pointer;
}

/* Smaller round icon button -- used inline within a row (e.g. a station's
   own header) rather than pinned to a modal's corner, where the standard
   .icon-btn size would dominate the row. */
.icon-btn-sm {
	width: 1.8rem;
	height: 1.8rem;
	font-size: 0.9rem;
	margin-left: 0;
}

.icon-btn:hover {
	color: var(--accent);
	background: rgba(213, 0, 0, 0.12);
}

main {
	display: flex;
	justify-content: center;
	align-items: center;
	min-height: calc(100vh - 58px);
	padding: 2rem 1rem;
}

.card {
	width: fit-content;
	max-width: 90vw;
	background: var(--panel);
	border: 1px solid var(--accent);
	border-radius: 8px;
	padding: 1.5rem 2rem;
	box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55);
}

.card h1 {
	margin: 0 0 1rem 0;
	text-align: left;
	color: var(--fg);
}

.card hr {
	border: none;
	border-top: 1px solid var(--accent);
	margin: 1rem 0;
}

.card p {
	line-height: 1.5;
	color: var(--fg);
}

.button-row {
	display: flex;
	justify-content: space-between;
	gap: 1rem;
	margin-top: 1rem;
}

.button-row button {
	width: 100px;
	padding: 0.5rem 0;
	background: var(--accent);
	color: #fff;
	border: none;
	border-radius: 4px;
	cursor: pointer;
	font-size: 0.95rem;
}

.button-row button:hover {
	filter: brightness(1.15);
}

.modal-overlay {
	position: fixed;
	inset: 0;
	background: rgba(0, 0, 0, 0.65);
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 1rem;
}

.modal {
	position: relative;
	background: var(--panel);
	border: 1px solid var(--accent);
	border-radius: 8px;
	padding: 1.5rem;
	max-width: 560px;
	width: 100%;
	box-shadow: 0 4px 18px rgba(0, 0, 0, 0.55);
}

.modal-icon-corner {
	position: absolute;
	top: 1rem;
	right: 1rem;
	display: flex;
	gap: 0.5rem;
}

.modal-icon-corner .icon-btn {
	width: 2rem;
	height: 2rem;
	font-size: 1rem;
}

.toggle-switch {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	cursor: pointer;
	user-select: none;
	color: var(--fg);
}

.toggle-switch input {
	display: none;
}

.toggle-slider {
	width: 36px;
	height: 20px;
	background: var(--bg);
	border: 1px solid var(--accent);
	border-radius: 10px;
	position: relative;
	transition: background 0.15s ease;
	flex-shrink: 0;
}

.toggle-slider::before {
	content: "";
	position: absolute;
	width: 14px;
	height: 14px;
	border-radius: 50%;
	background: var(--accent);
	top: 2px;
	left: 2px;
	transition: transform 0.15s ease, background 0.15s ease;
}

.toggle-switch input:checked + .toggle-slider {
	background: var(--accent);
}

.toggle-switch input:checked + .toggle-slider::before {
	transform: translateX(16px);
	background: #fff;
}

.modal h2 {
	margin-top: 0;
	color: var(--accent);
}

.dropzone {
	border: 2px dashed var(--accent);
	border-radius: 8px;
	padding: 2.5rem 1.5rem;
	text-align: center;
	cursor: pointer;
	color: var(--fg);
	transition: background 0.15s ease, border-color 0.15s ease;
}

.dropzone:hover, .dropzone:focus-visible {
	background: rgba(213, 0, 0, 0.06);
	outline: none;
}

.dropzone.dropzone-active {
	background: rgba(213, 0, 0, 0.12);
	border-color: var(--accent);
}

.dropzone-icon {
	font-size: 2.5rem;
	opacity: 0.7;
	margin-bottom: 0.5rem;
	display: block;
}

.dropzone p {
	margin: 0.25rem 0;
}

.modal p {
	line-height: 1.5;
}

.modal .close-btn {
	margin-top: 1rem;
	padding: 0.5rem 1.25rem;
	background: var(--accent);
	color: #fff;
	border: none;
	border-radius: 4px;
	cursor: pointer;
}

.modal .close-btn:hover {
	filter: brightness(1.15);
}

/* Reusable form elements -- used by login/register/setup and later by the
   field/site/station edit panels. */

.form-group {
	margin-bottom: 1rem;
	text-align: left;
}

/* Site editor: Name + its X/Y position-on-field-map fields side by side --
   X/Y are ~25% each, Name takes the rest. */
.site-name-pos-row {
	display: flex;
	gap: 0.75rem;
	align-items: flex-start;
}

.site-name-field {
	flex: 1 1 50%;
	min-width: 0;
}

.site-pos-field {
	flex: 0 0 25%;
	min-width: 0;
}

/* While the field's dots are locked, X/Y are shown as plain text instead
   of editable boxes (site-modal.js toggles the disabled attribute) --
   this overrides the disabled look to read as text, not a greyed-out
   control, since it's ID-scoped rather than a blanket .form-input:disabled
   rule (nothing else on this page uses disabled inputs). */
#site-edit-posx:disabled, #site-edit-posy:disabled {
	background: transparent;
	border-color: transparent;
	padding-left: 0;
	color: var(--fg);
	opacity: 1;
	cursor: default;
}

.form-group label {
	display: block;
	margin-bottom: 0.35rem;
	font-size: 0.9rem;
	color: var(--fg);
	opacity: 0.85;
}

.form-input {
	width: 100%;
	padding: 0.5rem 0.65rem;
	background: var(--bg);
	color: var(--fg);
	border: 1px solid var(--accent);
	border-radius: 4px;
	font-size: 1rem;
}

.form-input:focus {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

.btn {
	display: inline-block;
	padding: 0.55rem 1.25rem;
	background: var(--accent);
	color: #fff;
	border: none;
	border-radius: 4px;
	cursor: pointer;
	font-size: 0.95rem;
	text-decoration: none;
	text-align: center;
}

.btn:hover {
	filter: brightness(1.15);
}

.btn-block {
	display: block;
	width: 100%;
}

.btn-secondary {
	background: transparent;
	border: 1px solid var(--accent);
	color: var(--accent);
}

.message {
	padding: 0.65rem 0.9rem;
	border-radius: 4px;
	margin-bottom: 1rem;
	font-size: 0.9rem;
}

.message-error {
	background: rgba(213, 0, 0, 0.15);
	border: 1px solid var(--accent);
	color: var(--fg);
}

.message-success {
	background: rgba(46, 204, 64, 0.15);
	border: 1px solid #2ecc40;
	color: var(--fg);
}

.form-footer {
	margin-top: 1rem;
	font-size: 0.9rem;
	text-align: center;
	color: var(--fg);
	opacity: 0.85;
}

.form-footer a {
	color: var(--accent);
}

.auth-card {
	width: 100%;
	max-width: 380px;
}

.stack {
	display: flex;
	flex-direction: column;
	gap: 1.5rem;
	align-items: stretch;
}

/* Field map view -- no scrollbars anywhere on this page, image/placeholder
   scales to fit height first, then width if that would overflow. */

body.field-map-page {
	overflow: hidden;
	height: 100vh;
}

body.field-map-page main {
	display: block;
	padding: 2rem; /* margin around the map -- reveals the page's grid background */
	min-height: 0;
}

.map-container {
	position: relative;
	width: 100%;
	height: calc(100vh - 58px - 4rem); /* viewport minus topbar minus main's top+bottom padding */
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	background: transparent; /* let the page's grid background show through */
}

/* Built entirely by field.js: the map image and its dots share one
   coordinate space (the SVG's own viewBox, sized to the image's natural
   pixels), so preserveAspectRatio scales both together with zero drift --
   there's no separate img + absolutely-positioned-div layer to fall out
   of sync on resize, which was the actual bug in that older approach. */
.map-svg {
	width: 100%;
	height: 100%;
	user-select: none;
}

/* Field-map corner button stack -- fixed to the viewport (not the
   map-container, so it isn't clipped by its overflow:hidden), bottom-right,
   stacked top-to-bottom: add-site, devices, lock/unlock, back-to-dashboard. */

.map-corner-stack {
	position: fixed;
	z-index: 20;
	bottom: 1rem;
	right: 1rem;
	display: flex;
	flex-direction: column;
	gap: 0.75rem;
}

.map-corner-stack .icon-btn {
	background: var(--panel);
}

/* Mirrors .map-corner-stack on the opposite (bottom-left) corner -- the
   dot-size +/- buttons, a touch-friendly stand-in for the Ctrl+/Ctrl-
   keyboard shortcut on devices with no Ctrl key. */
.map-corner-stack-left {
	right: auto;
	left: 1rem;
}

/* Field-view datapoint summary badge -- counts active (non-disabled)
   datapoints that are currently reporting on time (green) vs. not
   (red/late or never_reported). Each side hides itself when its count is 0. */

.datapoint-summary {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	height: 2.5rem; /* matches .icon-btn (theme toggle / logout) so the topbar-nav row reads as one consistent height */
	box-sizing: border-box;
	background: var(--panel);
	border: 1px solid var(--accent);
	border-radius: 20px;
	padding: 0 0.8rem;
}

.datapoint-summary-item {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	color: var(--fg);
	font-weight: 600;
}

.summary-dot {
	width: 12px;
	height: 12px;
	border-radius: 50%;
	display: inline-block;
}

.summary-dot-green {
	background: #2ecc40;
}

.summary-dot-red {
	background: #e74c3c;
}

/* Site/Station status dots -- white+dashed by default, solid green once
   live, blinking red/orange when late. Now an SVG <circle> living inside
   .map-svg's own coordinate space (see field.js buildMapSvg()), so these
   are SVG presentation properties (fill/stroke) instead of the old
   HTML/CSS box-model ones (background/border) -- position/size come from
   the circle's own cx/cy/r attributes, set in JS, not from CSS at all. */

.status-dot {
	fill: #fff;
	stroke: #888;
	stroke-width: 2;
	stroke-dasharray: 4 3;
	cursor: pointer;
	pointer-events: auto;
	/* Keeps the outline a crisp, constant 2 screen-pixels regardless of the
	   uploaded image's native resolution -- only the fill (the circle's
	   actual radius, from DotSize) is meant to scale with the map; a raw
	   stroke-width in viewBox units would be an invisible hairline on a
	   large photo and a thick ring on a small placeholder. */
	vector-effect: non-scaling-stroke;
}

.status-dot.state-live {
	fill: #2ecc40;
	stroke: #2ecc40;
	stroke-dasharray: none;
}

.status-dot.state-late {
	stroke-dasharray: none;
	animation: dot-blink-late 1s steps(1, end) infinite;
}

.status-dot.dragging {
	cursor: grabbing;
	opacity: 0.85;
}

@keyframes dot-blink-late {
	0%, 49% { fill: #e74c3c; stroke: #e74c3c; }
	50%, 100% { fill: #f39c12; stroke: #f39c12; }
}

/* Station indicator dot -- same states, smaller, sits in a station
   rectangle's top-left corner. */

/* --station-scale is set inline (site-modal.js) on each .station-rect --
   scoped to that element's own subtree, so it only ever affects the dot/
   box sizing of station rects on the site MAP (view mode); the plain
   .station-indicator instances in the edit panel's station list (a
   different part of the DOM entirely) fall back to the default 1 and are
   never scaled by this. Font size is --station-font-scale (a per-site DB
   value) MULTIPLIED by this same --station-scale -- see .station-rect-title
   -- so it moves with the dot size too, on top of its own per-site value. */
.station-indicator {
	width: calc(12px * var(--station-scale, 1));
	height: calc(12px * var(--station-scale, 1));
	border-radius: 50%;
	background: #fff;
	border: 2px dashed #888;
	display: inline-block;
	flex-shrink: 0;
}

.station-indicator.state-live {
	background: #2ecc40;
	border: 2px solid #2ecc40;
}

.station-indicator.state-late {
	border-style: solid;
	animation: dot-blink-late 1s steps(1, end) infinite;
}

/* Setpoint status dots -- a DELIBERATELY separate class from
   .station-indicator (not just a modifier on it), because both
   site-modal.js's refreshOpenSiteIndicators() and edit-panel.js's
   refreshIndicators() re-stamp every ".station-indicator" element's
   className wholesale on each poll, positionally/by dataset.stationId --
   a setpoint dot sharing that class would get its color silently wiped
   every 2s. State vocabulary: matched (green, same meaning as live),
   pending (blue -- just commanded, waiting on the device's next report),
   mismatch (red/blinking, same treatment as late -- the device had a
   chance to apply it and still disagrees). */

.setpoint-indicator {
	width: 12px;
	height: 12px;
	border-radius: 50%;
	background: #fff;
	border: 2px dashed #888;
	display: inline-block;
	flex-shrink: 0;
}

.setpoint-indicator.state-matched {
	background: #2ecc40;
	border: 2px solid #2ecc40;
}

.setpoint-indicator.state-pending {
	background: #2196f3;
	border: 2px solid #2196f3;
}

.setpoint-indicator.state-mismatch {
	border-style: solid;
	animation: dot-blink-late 1s steps(1, end) infinite;
}

/* Larger modal variant for the site modal (map + edit views) and the device modal. */

.modal-lg {
	max-width: 800px;
	width: 95%;
	max-height: 90vh;
	overflow-y: auto;
}

/* The site modal specifically is allowed to grow much wider than the
   default modal-lg cap, so its frame can expand to match an uploaded map's
   own aspect ratio (see .site-map-container below) instead of squeezing/
   cropping the image into a fixed box. */
#site-modal-overlay .modal-lg {
	width: fit-content;
	min-width: min(500px, 90vw); /* keeps the edit view's forms/tables usable */
	max-width: 92vw;
}

/* History chart modal -- roughly 80% of the viewport in both dimensions,
   with the canvas filling whatever's left after the title/range buttons. */
.chart-modal {
	width: 80vw;
	height: 80vh;
	max-width: 80vw;
	max-height: 80vh;
	display: flex;
	flex-direction: column;
	overflow: hidden;
}

.chart-header-row {
	display: flex;
	align-items: baseline;
	gap: 0.75rem;
	flex-wrap: wrap;
}

.chart-header-row h2 {
	margin-bottom: 0;
}

.chart-live-value {
	font-size: 0.9rem;
	opacity: 0.8;
}

/* Every range button is the same width (sized to fit "35d", the widest
   label) so they line up evenly -- except Recent, which stays its own
   natural (wider) width since it's a different kind of thing ("everything
   currently retained," not a fixed time window). */
#history-chart-range-buttons .btn[data-range]:not([data-range="raw"]) {
	min-width: 3.75rem;
}

.chart-canvas-wrap {
	position: relative;
	flex: 1;
	min-height: 0; /* lets a flex child actually shrink instead of forcing the modal taller */
}

/* Inline Y-axis min/max editor, toggled by the chart modal's Edit (pencil)
   icon -- Editor+ only, see field.php's $canEdit gate. */
.chart-scale-form {
	display: flex;
	align-items: flex-end;
	gap: 0.75rem;
	flex-wrap: wrap;
	margin-bottom: 0.5rem;
}

.chart-scale-field {
	flex: 0 0 120px;
	margin-bottom: 0;
}

.chart-canvas-wrap canvas {
	width: 100% !important;
	height: 100% !important;
}

.modal-actions {
	display: flex;
	gap: 0.75rem;
	margin-top: 1rem;
}

.modal-actions .btn {
	flex: 1;
}

/* Shrink-wraps to the image's own rendered size (no fixed width/height),
   capped at ~80% of the viewport in each dimension (the "10% margin around
   it" the map should never exceed) -- the full map is always visible, never
   cropped. */
/* Wraps .site-map-container so the station-size +/- buttons can be
   positioned against the map's own rendered footprint (not the whole
   modal) while living OUTSIDE it -- buildImageMapSvg() does
   container.innerHTML = "" on every rebuild (new site, re-upload), which
   would silently delete the buttons if they were inside
   .site-map-container itself. */
.site-map-wrap {
	position: relative;
	display: inline-block;
}

.site-map-size-controls {
	position: absolute;
	z-index: 5;
	bottom: 0.5rem;
	left: 0.5rem;
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
}

.site-map-size-controls .icon-btn {
	background: var(--panel);
}

.site-map-container {
	position: relative;
	display: inline-block;
	min-width: 200px;
	min-height: 150px; /* never fully collapses, even if the SVG's own intrinsic sizing hiccups */
	max-width: 80vw;
	max-height: 70vh;
	overflow: hidden;
	background: var(--bg);
	border: 1px solid var(--accent);
	border-radius: 8px;
	line-height: 0;
}

.site-map-container .map-svg {
	display: block;
	max-width: 80vw;
	max-height: 70vh;
	/* width/height: auto (overriding the generic .map-svg 100%/100%) lets
	   this shrink-wrap to the SVG's own natural width/height attributes
	   (set in buildImageMapSvg()), the same way a plain <img> naturally
	   sizes from its own natural pixel dimensions. */
	width: auto;
	height: auto;
	user-select: none;
}

/* Station rectangle overlaid on a site map. */

/* Lives inside a <foreignObject> now (see site-modal.js buildStationRect())
   -- position comes from the foreignObject's own x/y in SVG space, not CSS
   left/top, so this is normal static flow, not position:absolute. Font
   size is a fixed standard rem size (.station-rect-title/-data td below),
   not scaled to the map. */
.station-rect {
	background: rgba(0, 0, 0, 0.75);
	border: 1px solid var(--accent);
	border-radius: 4px;
	cursor: grab;
	/* Width/Height/floors are set inline from JS (map-relative units) --
	   these are just pre-JS fallback defaults. */
	min-width: 90px;
	min-height: 44px;
	/* height:auto (the default) already shrink-wraps to content -- that's
	   why only the WIDTH was overflowing (e.g. "Oven_Right 20.4 *C" running
	   past the dark background's right edge). width:auto on a normal block
	   does the opposite of height:auto: it fills its containing block
	   (here, the foreignObject's own configured Width) rather than growing
	   for wider content, so a value/label wider than that floor had nowhere
	   to go but visibly outside the box. width:max-content makes it size to
	   its actual content instead, same as height already did -- min-width
	   above still applies as a floor either way (min-width always wins over
	   a smaller computed width, regardless of what width itself resolves to). */
	width: max-content;
	padding: 4px 6px;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
}

/* The foreignObject itself: overflow:visible is what lets .station-rect's
   min-width/min-height still grow past this box's nominal (Width/Height-
   derived) size to fit its actual content, instead of being clipped --
   foreignObject clips its content by default. */
.station-rect-foreign {
	overflow: visible;
}

.station-rect.dragging {
	cursor: grabbing;
	opacity: 0.85;
}

/* Dot on the far left, Title beside it. DOM order in site-modal.js is
   [dot, title] to match -- flex row puts the first child on the left. */
.station-rect-header {
	display: flex;
	align-items: center;
	gap: 0.35rem;
	flex-shrink: 0;
	pointer-events: none; /* the rect itself handles drag/click, not this label */
}

.station-rect-title {
	color: #fff;
	/* Font size combines BOTH multipliers: --station-font-scale (a PER-SITE
	   value, Sites.FontScale, same for every viewer) AND --station-scale
	   (the per-browser dot/box size preference, see .station-indicator
	   above) -- so nudging the dot size with the +/- buttons moves the
	   font with it too, on top of whatever this site's own FontScale is.
	   Both set inline on .station-rect in site-modal.js's buildStationRect(). */
	font-size: calc(0.75rem * var(--station-font-scale, 1) * var(--station-scale, 1));
	font-weight: 600;
	text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
	/* Without this, a flex item with nowrap text refuses to shrink below
	   its full text width (the default min-width:auto browser behavior). */
	min-width: 0;
	flex: 1;
	/* NOT white-space:nowrap + overflow:hidden + text-overflow:ellipsis
	   (as this used to be): inside a foreignObject nested in an SVG, that
	   combination collapsed this element's rendered height to 0 -- the
	   title was still there (correct width, per devtools) but invisible,
	   0px tall. overflow:hidden is specifically what triggered it (removing
	   only that, confirmed live, made the title reappear); text-overflow:
	   ellipsis can't do anything without it anyway. A long title just wraps
	   onto a second line now instead of truncating -- the station box
	   already grows to fit its content (see .station-rect-foreign's
	   overflow:visible), so there's nowhere for it to be clipped either way. */
}

/* Borderless mini datapoints table shown inside the rectangle itself. */
.station-rect-data {
	margin-top: 3px;
	border-collapse: collapse;
	width: 100%;
	pointer-events: none;
}

.station-rect-data td {
	color: #fff;
	font-size: calc(0.65rem * var(--station-font-scale, 1) * var(--station-scale, 1));
	line-height: 1.3;
	padding: 0;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	max-width: calc(80px * var(--station-font-scale, 1) * var(--station-scale, 1));
	text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}

/* Label cell specifically -- a bit of breathing room before the value,
   not just the 3px this used to share with every cell (including the last
   one, where it did nothing useful). */
.station-rect-data td:first-child {
	padding-right: 8px;
}

/* The table itself ignores pointer events (so drag-to-move the station
   rect still works when grabbing anywhere else in it), but a value cell
   re-enables them -- clicking a value opens its history chart. */
.station-rect-value {
	pointer-events: auto;
	cursor: pointer;
}

.station-rect-value:hover {
	text-decoration: underline;
}

/* Writer+ setpoint quick-edit icon in the station rect (view mode) -- see
   site-modal.js's buildStationRect(). Same pointer-events override reason
   as .station-rect-value above. */
.setpoint-edit-btn {
	pointer-events: auto;
	cursor: pointer;
	background: none;
	border: none;
	color: var(--fg);
	opacity: 0.7;
	font-size: 0.75rem;
	padding: 0 0.2rem;
}

.setpoint-edit-btn:hover {
	opacity: 1;
	color: var(--accent);
}

.setpoint-row-dot {
	width: 8px;
	height: 8px;
	margin-right: 4px;
	vertical-align: middle;
}

/* Collapsible station sections in the edit view -- native <details> for
   simplicity/accessibility instead of hand-rolled toggle JS. */

.station-details {
	border: 1px solid var(--accent);
	border-radius: 6px;
	margin-bottom: 0.75rem;
	overflow: hidden;
}

.station-details summary {
	padding: 0.6rem 0.9rem;
	cursor: pointer;
	background: var(--panel);
	font-weight: 600;
	list-style: none;
}

.station-details summary::-webkit-details-marker {
	display: none;
}

.station-details summary::before {
	content: "\25B8";
	display: inline-block;
	margin-right: 0.5rem;
	transition: transform 0.15s ease;
}

.station-details[open] summary::before {
	transform: rotate(90deg);
}

/* Status dot on the left, editable Title on the right -- the marker
   above sits before this row. */
.station-block-header {
	display: flex;
	align-items: center;
	gap: 0.6rem;
}

.station-title-input {
	flex: 1;
	min-width: 0;
	text-align: left;
	background: var(--bg);
	border: 1px solid var(--accent);
	color: var(--fg);
	font-weight: 600;
	font-size: 1rem;
	padding: 0.3rem 0.5rem;
	border-radius: 4px;
}

.station-title-input:focus {
	outline: none;
	background: var(--panel);
}

.station-details-body {
	padding: 0.6rem 0.75rem;
}

.station-subsection-heading {
	margin: 0.75rem 0 0.35rem;
	font-size: 0.85rem;
	opacity: 0.75;
	text-transform: uppercase;
	letter-spacing: 0.03em;
}

.station-subsection-heading:first-child {
	margin-top: 0;
}

.datapoints-table {
	width: 100%;
	border-collapse: collapse;
	margin-bottom: 0.5rem;
}

.datapoints-table th, .datapoints-table td {
	border: none;
	text-align: left;
	padding: 0.3rem 0.4rem;
	font-size: 0.85rem;
}

.datapoints-table th {
	opacity: 0.7;
	font-weight: 600;
}

.datapoints-table select, .datapoints-table input {
	background: var(--bg);
	color: var(--fg);
	border: 1px solid var(--accent);
	border-radius: 4px;
	padding: 0.2rem 0.4rem;
	width: 100%;
}

/* Interval/Unit/Scale/Decimals columns are all fixed-width, sized to what
   they actually need to hold -- letting them stretch to width:100% (the
   generic rule above) is what made this table so much wider than its
   content, since the browser then hands leftover column space to whatever
   cell can absorb it. */

/* Interval's actual width is set inline in JS (edit-panel.js's
   sizeIntervalInput()) to hug however many digits are currently typed.
   min-width/max-width below are in PX, not ch: under the global
   box-sizing:border-box (see "* { box-sizing: border-box }"), a ch-based
   min-width includes this input's own padding+border, so a small enough
   ch value leaves virtually no room for the actual digit glyph -- a value
   of "0" or "-1" could render as an apparently-empty box. A px floor
   comfortably wide enough to fit a couple of digits plus that padding/
   border sidesteps the problem entirely; max-width is likewise px so nothing
   can grow unbounded mid-edit. */
.interval-input {
	width: 3ch;
	min-width: 40px;
	max-width: 64px;
	flex: none;
}

/* The REAL reason this was unreadable: these are type="number" inputs, and
   the browser's native up/down spinner arrows eat a fixed chunk of width
   (~16-20px) regardless of how narrow the box is -- at these box widths
   that left almost nothing for the digit itself, squeezing "0" or "-1"
   into an unreadable sliver next to the arrows. Widening the box alone
   (previous fix) wasn't enough because the arrows just kept eating a
   share of whatever extra room that made. Hiding them reclaims that space
   for the actual number; the value is still fully editable by typing
   (and via Save all), nothing here depends on the spinner buttons. */
.interval-input::-webkit-outer-spin-button,
.interval-input::-webkit-inner-spin-button,
.decimals-input::-webkit-outer-spin-button,
.decimals-input::-webkit-inner-spin-button,
.scale-input::-webkit-outer-spin-button,
.scale-input::-webkit-inner-spin-button {
	-webkit-appearance: none;
	margin: 0;
}

.interval-input,
.decimals-input,
.scale-input {
	-moz-appearance: textfield;
}

.refresh-value {
	font-size: 0.85rem;
	opacity: 0.85;
	white-space: nowrap;
}

.unit-input {
	width: 8ch;
	min-width: 56px;
	max-width: 96px;
	flex: none;
}

/* 5 integer digits + "." + 3 decimal digits = 9 characters. */
.scale-input {
	width: 9ch;
	min-width: 72px;
	max-width: 112px;
	flex: none;
}

.decimals-input {
	width: 3ch;
	min-width: 32px;
	max-width: 48px;
	flex: none;
}

.add-datapoint-row {
	display: flex;
	gap: 0.5rem;
	align-items: center;
	margin-top: 0.5rem;
}

.add-datapoint-row select {
	flex: 1;
	background: var(--bg);
	color: var(--fg);
	border: 1px solid var(--accent);
	border-radius: 4px;
	padding: 0.35rem;
}

.field-hint {
	font-size: 0.7rem;
	opacity: 0.65;
	margin-top: 0.2rem;
}

.datapoint-row-actions {
	display: flex;
	gap: 0.35rem;
}

.datapoint-last-updated {
	white-space: nowrap;
	opacity: 0.85;
	font-size: 0.8rem;
}

/* Brief dim/flash whenever a live value changes, in either the site map's
   station rectangles or the edit panel's datapoint table. */
@keyframes value-flash {
	0% { opacity: 0.35; }
	100% { opacity: 1; }
}

.value-flash {
	animation: value-flash 300ms ease-out;
}

/* ------------------------------------------------------------------ */
/* Mobile / responsive                                                */
/* ------------------------------------------------------------------ */

.rotate-overlay {
	display: none;
	position: fixed;
	inset: 0;
	z-index: 9999;
	background: var(--bg);
	color: var(--fg);
	flex-direction: column;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: 2rem;
}

.rotate-overlay-icon {
	font-size: 3rem;
	margin-bottom: 1rem;
	animation: rotate-hint 1.8s ease-in-out infinite;
}

@keyframes rotate-hint {
	0%, 100% { transform: rotate(0deg); }
	50% { transform: rotate(90deg); }
}

/* Only gates phone/tablet-sized viewports -- a desktop browser window that
   happens to be taller than wide (e.g. a narrow split-screen) shouldn't be
   blocked, only actual portrait-held mobile devices. */
@media (orientation: portrait) and (max-width: 900px) {
	.rotate-overlay {
		display: flex;
	}
}

/* Tables with more columns than a phone screen can show (datapoints-table,
   devices-table) scroll horizontally within their own row instead of
   squeezing every column unreadably thin or breaking the page layout. */
.table-scroll {
	overflow-x: auto;
	-webkit-overflow-scrolling: touch;
}

/* Admin panel: fields/users tables -- generous padding and a visible row
   separator so a dense list stays easy to scan, plus a hover highlight to
   make the currently-under-the-cursor row unambiguous. */
.admin-table {
	width: 100%;
	border-collapse: collapse;
}

.admin-table th, .admin-table td {
	text-align: left;
	padding: 0.75rem 1rem;
	border-bottom: 1px solid var(--grid-line);
}

.admin-table th {
	opacity: 0.7;
	font-weight: 600;
	font-size: 0.8rem;
	text-transform: uppercase;
	letter-spacing: 0.03em;
}

.admin-table tbody tr:hover {
	background: rgba(213, 0, 0, 0.05);
}

.admin-table tfoot td {
	padding: 0.85rem 1rem;
	border-top: 2px solid var(--accent);
	border-bottom: none;
}

.admin-row-actions {
	display: flex;
	gap: 0.4rem;
}

/* Expandable edit row, inserted directly below the row it edits (colspan
   across the whole table) -- toggled by that row's own Edit button. */
.admin-detail-row td {
	background: var(--bg);
	padding: 1rem;
}

.admin-detail-fields {
	display: flex;
	gap: 0.75rem;
	flex-wrap: wrap;
	margin-bottom: 0.75rem;
	align-items: flex-end;
}

.admin-detail-fields .form-group {
	flex: 1 1 160px;
	margin-bottom: 0;
}

.admin-access-list {
	font-size: 0.85rem;
	opacity: 0.85;
	margin: 0.5rem 0;
}

.admin-grant-table {
	width: 100%;
	border-collapse: collapse;
	margin-top: 0.5rem;
	font-size: 0.9rem;
}

.admin-grant-table td {
	padding: 0.35rem 0.5rem;
	border-bottom: 1px solid var(--grid-line);
}

.admin-grant-table input[type="number"] {
	width: 4.5rem;
}

.admin-grant-table select {
	background: var(--bg);
	color: var(--fg);
	border: 1px solid var(--accent);
	border-radius: 4px;
	padding: 0.3rem;
}

/* Shared replacement for the old ad-hoc inline flex button rows -- wraps
   instead of overflowing on narrow viewports. */
.btn-row {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
}

@media (max-width: 640px) {
	.topbar {
		padding: 0.6rem 0.75rem;
	}

	.brand {
		font-size: 1.15rem;
	}

	.topbar-field-title {
		max-width: 30vw;
		font-size: 0.9rem;
	}

	.topbar-center {
		gap: 0.5rem;
	}

	.icon-btn {
		width: 2.2rem;
		height: 2.2rem;
		font-size: 1.1rem;
	}

	main {
		padding: 1rem 0.5rem;
	}

	.map-corner-stack {
		bottom: 0.5rem;
		right: 0.5rem;
		gap: 0.5rem;
	}

	.map-corner-stack-left {
		right: auto;
		left: 0.5rem;
	}

	.datapoint-summary {
		gap: 0.4rem;
		height: 2.2rem; /* matches the mobile .icon-btn size above */
		padding: 0 0.6rem;
	}

	.modal {
		padding: 1rem;
	}
}
