mobile: fix edge beam (transform-rotate) + ONU master-detail layout

Edge glow: iOS Safari won't animate an @property-driven conic-gradient
angle, so the beam sat static / off the border on mobile. Reworked to the
robust technique: a .beam child masked to the border ring contains an
oversized conic-gradient ::before spun with transform: rotate 0→360deg; the
glow halo is a drop-shadow on the parent (survives the child mask). Works on
iOS/Safari; honors prefers-reduced-motion.

ONU info on narrow screens: was stacking the picker above the detail, so
selecting an ONU meant a long scroll to find it. Now master-detail — show
the picker OR the detail (not both), toggled by .show-detail on
#view-onu-info via a shared @media(max-width:860px) rule; selecting shows the
detail with a "← ONU list" back button, tab/sub-tab nav resets to the
picker. (Earlier commit already de-compacted the list: extra filters +
two-line rows.)

Docs: CLAUDE.md §15 (edge glow technique) + §17 (master-detail).

Verified offscreen at 414px: beam element present; panel-left/​panel-main
display toggles correctly on select/back; back button visible on mobile;
rendered both states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon Vanvik 2026-06-23 16:33:07 +02:00
parent 2046fad0f8
commit 6f4a315947
4 changed files with 81 additions and 31 deletions

View file

@ -416,39 +416,55 @@ button.danger:hover:not(:disabled) {
.modal-type-label { display: block; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 2px; }
/* -------- Animated neon edge glow (beam travels around the border) -------- */
@property --beam-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
/* -------- Animated neon edge glow (beam travels around the border) --------
A rotating conic-gradient (transform: rotate works on iOS Safari, unlike
an @property-animated gradient angle) clipped to a thin border ring. The
glow halo is a drop-shadow on the parent so it survives the child's mask. */
#edge-glow {
position: fixed;
inset: 0;
z-index: 9998;
pointer-events: none;
filter: drop-shadow(0 0 6px var(--neon)) drop-shadow(0 0 16px var(--cyan));
}
#edge-glow .beam {
position: absolute;
inset: 0;
padding: 3px; /* thickness of the glowing border band */
background: conic-gradient(
from var(--beam-angle),
transparent 0deg,
transparent 38deg,
var(--cyan) 64deg,
#ffffff 90deg,
var(--neon) 116deg,
transparent 142deg,
transparent 360deg
);
overflow: hidden;
/* Keep only the padding ring (border band); punch out the interior. */
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
filter: drop-shadow(0 0 8px var(--neon)) drop-shadow(0 0 22px var(--cyan));
animation: beamspin 7s linear infinite;
}
@keyframes beamspin { to { --beam-angle: 360deg; } }
#edge-glow .beam::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 200vmax; /* > viewport diagonal at any rotation */
height: 200vmax;
transform: translate(-50%, -50%);
background: conic-gradient(
from 0deg,
transparent 0deg,
transparent 40deg,
var(--cyan) 66deg,
#ffffff 90deg,
var(--neon) 114deg,
transparent 140deg,
transparent 360deg
);
animation: beamspin 7s linear infinite;
will-change: transform;
}
@keyframes beamspin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
#edge-glow { animation: none; }
#edge-glow .beam::before { animation: none; }
}
/* -------- Top tabs -------- */
@ -562,6 +578,19 @@ svg.spark { vertical-align: middle; opacity: 0.9; }
.olt-modal { width: min(960px, 95vw); }
.olt-modal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
.olt-onu-list { max-height: 42vh; margin-top: 6px; }
/* "Back to list" button only matters in the narrow master-detail layout. */
.onu-detail-back { display: none; align-self: flex-start; }
/* Narrow screens: the ONU info page is master-detail show the picker OR
the selected ONU's detail, never both stacked (which buried the detail
under a long list). `.show-detail` is toggled in app.js on select / back. */
@media (max-width: 860px) {
#view-onu-info .panel-main { display: none; }
#view-onu-info.show-detail .panel-left { display: none; }
#view-onu-info.show-detail .panel-main { display: flex; flex-direction: column; }
#view-onu-info .onu-detail-back { display: inline-block; }
}
.detail-card {
background: var(--bg-elev);
border: 1px solid var(--border);