The page revamped: icon navigation, hints instead of prose, one Settings page, a weekly calendar, a PIN

- Top bar with icon navigation (Overview, Keys, Computers, Settings, Logs, History, Help); no side
  menu; every explanation moved into a hover hint; the [how] lines no longer shift the layout.
- One Settings page with sections: Playlist, Auto-fix (a paintable weekly calendar + which fixes),
  Changes wait (calendar), HyperDecks, MIDI links, Computers, Dante, Switcher, Companion (with the
  names check), Network, Site & PIN, Program & campus, Advanced (every field by screen).
- Fixes live under Keys as a tab; the jobs became sections or + buttons where they belong.
- Editing is switched on with a PIN (digits) plus a first name for History; the overview's
  status line became chips; 'Needs attention' instead of a person's name, and it no longer fires
  after a page-only Apply (page-only.json marker); last change comes from History.
- Deployed to NA Ingest; the campus PIN set there and in config.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 17:42:03 -04:00
co-authored by Claude Fable 5.1
parent 9278e08acc
commit 69bb09ad27
5 changed files with 561 additions and 467 deletions
+8
View File
@@ -462,8 +462,16 @@ class Applier:
self._rollback(entry, xml_backup, plan, old_cfg, detail) self._rollback(entry, xml_backup, plan, old_cfg, detail)
return return
self.step("restart", "ok", detail) self.step("restart", "ok", detail)
try:
os.remove(os.path.join(site.data_dir, "page-only.json"))
except OSError:
pass
else: else:
self.step("restart", "skipped", "nothing the watchdog reads changed") self.step("restart", "skipped", "nothing the watchdog reads changed")
started = site.read_json("started.json", {}) or {}
prev = site.read_json("page-only.json", {}) or {}
sp.write_json(os.path.join(site.data_dir, "page-only.json"),
{"sha": sha_after, "watchdog_sha": prev.get("watchdog_sha") if prev.get("watchdog_sha") == started.get("config_sha") else started.get("config_sha")})
# 4. pages # 4. pages
if plan["pages"]: if plan["pages"]:
self.step("pages", "running") self.step("pages", "running")
+8 -6
View File
@@ -196,7 +196,9 @@ class Site:
"level": "warn" if s.get("b") == 0xFFA000 else "err", "acked": s.get("b") == 0x8B0000, "since": None, "level": "warn" if s.get("b") == 0xFFA000 else "err", "acked": s.get("b") == 0x8B0000, "since": None,
"action": "hold the %s key (%s)" % (tile["label"], hold) if hold and not hold.startswith("nothing") "action": "hold the %s key (%s)" % (tile["label"], hold) if hold and not hold.startswith("nothing")
else "no automatic fix: check the device"}) else "no automatic fix: check the device"})
if started and started.get("config_sha") and started["config_sha"] != self.cfg_sha: page_only = self.read_json("page-only.json", {}) or {} # the last Apply changed nothing the watchdog reads
if started and started.get("config_sha") and started["config_sha"] != self.cfg_sha \
and not (page_only.get("sha") == self.cfg_sha and page_only.get("watchdog_sha") == started["config_sha"]):
needs.append("The watchdog is running an older settings file than the one on disk (restart it to pick up the change).") needs.append("The watchdog is running an older settings file than the one on disk (restart it to pick up the change).")
pages = self.pages_state() pages = self.pages_state()
if pages.get("status") and not pages["status"]["ok"]: if pages.get("status") and not pages["status"]["ok"]:
@@ -314,12 +316,12 @@ class Site:
return self.cached("pages", 60, look) return self.cached("pages", 60, look)
def last_change(self): def last_change(self):
baks = sorted(glob.glob(os.path.join(self.data_dir, "config.json.bak-*"))) entries = applying.history_entries(self.data_dir)
if not baks: if not entries:
return None return None
m = re.search(r"bak-(\d{8})-(\d{6})$", baks[-1]) e = entries[0]
return {"when": "%s-%s-%s %s:%s" % (m.group(1)[:4], m.group(1)[4:6], m.group(1)[6:], m.group(2)[:2], m.group(2)[2:4]) if m else None, return {"when": time.strftime("%Y-%m-%d %H:%M", time.localtime(e["when"])) if e.get("when") else None, "by": e.get("by"), "note": e.get("note"),
"by": "the installer", "note": "config.json replaced"} "outcome": e.get("outcome")}
def companion_view(self): def companion_view(self):
cfg = self.load() cfg = self.load()
+437 -374
View File
File diff suppressed because it is too large Load Diff
+18 -22
View File
@@ -8,36 +8,32 @@
</head> </head>
<body> <body>
<header class="top"> <header class="top">
<div class="brand"><span class="dot" id="hbdot" title="watchdog heartbeat"></span><b>STATUS PANEL</b> <span id="sitename" class="muted"></span></div> <a class="brand" href="#overview"><span class="dot" id="hbdot" data-hint="watchdog heartbeat"></span><b>STATUS PANEL</b><span id="sitename" class="muted"></span></a>
<input id="search" type="search" placeholder="find a key, computer, device, fix or setting" aria-label="search" autocomplete="off"> <nav class="nav" id="nav">
<a href="#overview" data-hint="Is the room OK"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="8" rx="1.5"/><rect x="3" y="13" width="8" height="8" rx="1.5"/><rect x="13" y="13" width="8" height="8" rx="1.5"/></svg><span>Overview</span></a>
<a href="#keys" data-hint="The deck: what each key watches and does"><svg viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h2M10 10h2M14 10h2M18 10h0M6 14h12"/></svg><span>Keys</span></a>
<a href="#computers" data-hint="What runs where"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg><span>Computers</span></a>
<a href="#settings" data-hint="Everything you can change"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3h0a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8v0a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg><span>Settings</span></a>
<a href="#logs" data-hint="What the watchdog did and saw"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg><span>Logs</span></a>
<a href="#history" data-hint="Every version ever applied"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span>History</span></a>
<a href="#help" data-hint="How it works, the cheat sheet"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.7.4-1 1-1 1.7M12 17h0"/></svg><span>Help</span></a>
</nav>
<input id="search" type="search" placeholder="find…" aria-label="search" autocomplete="off">
<div class="topright"> <div class="topright">
<button class="link" id="legendbtn" title="What the status words mean">?</button> <button class="icon" id="legendbtn" data-hint="What the colours mean">?</button>
<button id="editbtn" class="toggle">Editing: off</button> <button id="editbtn" class="toggle" data-hint="Switch Editing on with the PIN"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg><span>Edit</span></button>
</div> </div>
</header> </header>
<div id="legend" class="legend" hidden> <div id="legend" class="legend" hidden>
<b>Status words, the same everywhere:</b>
<span><i class="l-ok"></i>OK</span> <span><i class="l-ok"></i>OK</span>
<span><i class="l-problem"></i>Problem (with the reason)</span> <span><i class="l-problem"></i>Problem</span>
<span><i class="l-unknown"></i>Can't see from here – ask at the computer</span> <span><i class="l-unknown"></i>Can't see from here</span>
<span><i class="l-none"></i>Not needed</span> <span><i class="l-none"></i>Not needed</span>
<span><i class="l-hand"></i>Done by hand (who, when)</span> <span><i class="l-hand"></i>Done by hand</span>
<span><i class="l-purple"></i>Watchdog down (every key purple)</span>
</div> </div>
<div id="searchresults" class="searchresults" hidden></div> <div id="searchresults" class="searchresults" hidden></div>
<div class="shell"> <main id="main"><p class="muted">Loading…</p></main>
<nav class="menu" id="menu">
<a href="#overview">Overview</a>
<a href="#keys">Keys</a>
<a href="#computers">Computers</a>
<a href="#logs">Logs</a>
<a href="#history">History</a>
<a href="#help">Help</a>
<div class="divider">Set up once</div>
<a href="#site">Site setup</a>
<a href="#fixes">Fixes</a>
</nav>
<main id="main"><p class="muted">Loading…</p></main>
</div>
<div id="dialog" class="dialog" hidden></div> <div id="dialog" class="dialog" hidden></div>
<script src="/app.js"></script> <script src="/app.js"></script>
</body> </body>
+90 -65
View File
@@ -1,4 +1,4 @@
/* Layout: a fixed top bar, a narrow menu on the left, the screen on the right; one column on a phone. */ /* One top bar with icon navigation; the screen below; one column on a phone. Short labels, hints on hover. */
:root { :root {
--bg: #f4f6f3; --surface: #ffffff; --fg: #1d2420; --muted: #5d6963; --line: #d5dcd6; --accent: #0f6b3a; --accent-soft: #e1f0e7; --bg: #f4f6f3; --surface: #ffffff; --fg: #1d2420; --muted: #5d6963; --line: #d5dcd6; --accent: #0f6b3a; --accent-soft: #e1f0e7;
--deck: #1b1f1d; --ok: #007a2e; --problem: #d8322b; --warn: #e3a000; --unknown: #8b96a0; --none: #c3c9c3; --hand: #2b6cb0; --purple: #6a1b9a; --deck: #1b1f1d; --ok: #007a2e; --problem: #d8322b; --warn: #e3a000; --unknown: #8b96a0; --none: #c3c9c3; --hand: #2b6cb0; --purple: #6a1b9a;
@@ -12,90 +12,115 @@
--deck: #0c0e0d; --ok: #3fbf6f; --problem: #ff6a5e; --warn: #f0b84a; --unknown: #8b96a0; --none: #4c5553; --hand: #7fb3ef; color-scheme: dark } --deck: #0c0e0d; --ok: #3fbf6f; --problem: #ff6a5e; --warn: #f0b84a; --unknown: #8b96a0; --none: #4c5553; --hand: #7fb3ef; color-scheme: dark }
* { box-sizing: border-box; } * { box-sizing: border-box; }
[hidden] { display: none !important; } [hidden] { display: none !important; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.45 var(--font); } body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.4 var(--font); }
.top { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--surface); border-bottom: 1px solid var(--line); } svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.brand { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.brand b { letter-spacing: .06em; } /* top bar */
.top { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; display: flex; align-items: center; gap: 10px; padding: 6px 14px; background: var(--surface); border-bottom: 1px solid var(--line); }
.brand { display: flex; align-items: center; gap: 8px; white-space: nowrap; color: var(--fg); text-decoration: none; }
.brand b { letter-spacing: .06em; } .brand .muted { font-size: .9rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--unknown); display: inline-block; } .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--unknown); display: inline-block; }
.dot.ok { background: var(--ok); } .dot.bad { background: var(--purple); } .dot.ok { background: var(--ok); } .dot.bad { background: var(--purple); }
#search { flex: 1; min-width: 0; padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); font: inherit; } .nav { display: flex; gap: 2px; margin-left: 6px; }
.topright { display: flex; gap: 8px; align-items: center; } .nav a { display: flex; flex-direction: column; align-items: center; gap: 1px; padding: 4px 10px; border-radius: 8px; color: var(--muted); text-decoration: none; font-size: .72rem; }
button { font: inherit; cursor: pointer; border: 1px solid var(--line); background: var(--surface); color: var(--fg); border-radius: 8px; padding: 6px 12px; } .nav a svg { width: 22px; height: 22px; }
.nav a.active, .nav a:hover { background: var(--accent-soft); color: var(--accent); }
#search { flex: 1; min-width: 80px; max-width: 320px; margin-left: auto; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); font: inherit; }
.topright { display: flex; gap: 6px; align-items: center; }
button { font: inherit; cursor: pointer; border: 1px solid var(--line); background: var(--surface); color: var(--fg); border-radius: 8px; padding: 6px 12px; display: inline-flex; align-items: center; gap: 6px; }
button:hover { border-color: var(--accent); } button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } button:hover { border-color: var(--accent); } button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); } button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
button.link { border: 0; background: none; color: var(--accent); padding: 4px 8px; font-weight: 700; } button.icon { width: 34px; height: 34px; padding: 0; justify-content: center; border-radius: 50%; font-weight: 700; }
button.toggle { font-weight: 600; } button.toggle.on { background: var(--accent-soft); border-color: var(--accent); } button.toggle { font-weight: 600; } button.toggle.on { background: var(--accent); color: #fff; border-color: var(--accent); }
button[disabled] { opacity: .5; cursor: not-allowed; } button.danger { border-color: var(--problem); color: var(--problem); } button.primary.danger { background: var(--problem); color: #fff; }
.legend { padding: 8px 16px; background: var(--accent-soft); border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: .92rem; } button.small { padding: 3px 9px; font-size: .85rem; }
button[disabled] { opacity: .45; cursor: not-allowed; }
.badge { display: inline-block; padding: 5px 10px; border-radius: 999px; background: var(--accent); color: #fff; text-decoration: none; font-weight: 600; font-size: .85rem; white-space: nowrap; }
.badge.warn { background: var(--warn); color: #1d2420; }
.legend { padding: 6px 14px; background: var(--accent-soft); border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: .88rem; }
.legend i, .light { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; background: var(--unknown); } .legend i, .light { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; background: var(--unknown); }
.l-ok, .light.ok { background: var(--ok); } .l-problem, .light.problem { background: var(--problem); } .l-unknown, .light.unknown { background: var(--unknown); } .l-ok, .light.ok { background: var(--ok); } .l-problem, .light.problem { background: var(--problem); } .l-unknown, .light.unknown { background: var(--unknown); }
.l-none, .light.not_needed { background: var(--none); } .l-hand, .light.hand { background: var(--hand); } .light.warn { background: var(--warn); } .l-none, .light.not_needed { background: var(--none); } .l-hand, .light.hand { background: var(--hand); } .light.warn { background: var(--warn); } .l-purple { background: var(--purple); }
.shell { display: grid; grid-template-columns: 170px 1fr; min-height: calc(100vh - 50px); }
.menu { padding: 12px 8px; border-right: 1px solid var(--line); display: flex; flex-direction: column; gap: 2px; } /* hints: a small circle; the text appears on hover or focus, never in the flow */
.menu a { display: block; padding: 8px 10px; border-radius: 8px; color: var(--fg); text-decoration: none; } .hint { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; border-radius: 50%; border: 1px solid var(--muted); color: var(--muted); font-size: 10px; font-style: italic; font-family: Georgia, serif; margin-left: 5px; vertical-align: 1px; cursor: help; position: relative; }
.menu a.active, .menu a:hover { background: var(--accent-soft); color: var(--accent); } [data-hint] { position: relative; }
.menu .divider { margin: 14px 10px 4px; font-size: .74rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); border-top: 1px solid var(--line); padding-top: 10px; } [data-hint]:hover::after, [data-hint]:focus-visible::after { content: attr(data-hint); position: absolute; left: 50%; top: calc(100% + 6px); transform: translateX(-50%); z-index: 20; background: #1d2420; color: #fff; padding: 6px 9px; border-radius: 6px; font: .8rem/1.35 var(--font); font-style: normal; white-space: normal; width: max-content; max-width: 280px; box-shadow: 0 6px 18px rgba(0,0,0,.3); pointer-events: none; text-align: left; }
main { padding: 18px 20px 48px; min-width: 0; max-width: 1200px; } .hintleft[data-hint]:hover::after { left: auto; right: 0; transform: none; }
h1 { font-size: 1.5rem; margin: 0 0 4px; } h2 { font-size: 1.15rem; margin: 22px 0 8px; } h3 { font-size: 1rem; margin: 14px 0 6px; }
.what { color: var(--muted); margin: 0 0 14px; max-width: 70ch; } /* layout */
.muted { color: var(--muted); } .small { font-size: .88rem; } .mono { font-family: var(--mono); font-size: .88em; } main { padding: 14px 18px 48px; max-width: 1240px; margin: 0 auto; min-width: 0; }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 10px 0; min-width: 0; } h1 { font-size: 1.35rem; margin: 0 0 10px; display: flex; align-items: center; gap: 8px; } h2 { font-size: 1.05rem; margin: 18px 0 8px; } h3 { font-size: .95rem; margin: 10px 0 6px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; } .muted { color: var(--muted); } .small { font-size: .86rem; } .mono { font-family: var(--mono); font-size: .86em; }
.row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px; margin: 10px 0; min-width: 0; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; }
.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.cols { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 14px; } .cols > * { min-width: 0; } .cols { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 14px; } .cols > * { min-width: 0; }
@media (max-width: 900px) { .cols { grid-template-columns: minmax(0, 1fr); } } @media (max-width: 900px) { .cols { grid-template-columns: minmax(0, 1fr); } }
table { border-collapse: collapse; width: 100%; font-size: .93rem; } th, td { text-align: left; vertical-align: top; padding: 6px 9px; border-bottom: 1px solid var(--line); } table { border-collapse: collapse; width: 100%; font-size: .9rem; } th, td { text-align: left; vertical-align: middle; padding: 5px 8px; border-bottom: 1px solid var(--line); }
th { color: var(--muted); font-weight: 600; font-size: .85rem; } tr.click { cursor: pointer; } tr.click:hover { background: var(--accent-soft); } th { color: var(--muted); font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; } tr.click { cursor: pointer; } tr.click:hover { background: var(--accent-soft); } tr.sel { background: var(--accent-soft); }
.tablewrap { overflow-x: auto; } .tablewrap { overflow-x: auto; }
.word { font-weight: 700; } .word.OK { color: var(--ok); } .word.Problem { color: var(--problem); } .word.Not { color: var(--muted); } .word { font-weight: 700; } .word.OK { color: var(--ok); } .word.Problem { color: var(--problem); } .word.Not { color: var(--muted); }
.problems { list-style: none; padding: 0; margin: 0; } .problems li { padding: 6px 0; border-bottom: 1px dashed var(--line); } .problems { list-style: none; padding: 0; margin: 0; } .problems li { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; padding: 6px 0; border-bottom: 1px dashed var(--line); }
.problems .lvl { display: inline-block; min-width: 10px; height: 10px; border-radius: 3px; margin-right: 8px; } .problems .lvl { display: inline-block; width: 10px; height: 10px; border-radius: 3px; }
.pill { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--accent-soft); font-size: .85rem; font-family: var(--mono); } .pill { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--accent-soft); font-size: .82rem; font-family: var(--mono); }
.chips { display: flex; flex-wrap: wrap; gap: 6px; } .chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); font-size: .85rem; }
.chip.ok { border-color: var(--ok); } .chip.bad { border-color: var(--problem); color: var(--problem); }
.deck { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 5px; background: var(--deck); padding: 8px; border-radius: 12px; max-width: 640px; width: 100%; } .deck { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 5px; background: var(--deck); padding: 8px; border-radius: 12px; max-width: 640px; width: 100%; }
.key { aspect-ratio: 1; min-width: 0; overflow: hidden; border-radius: 6px; display: flex; align-items: center; justify-content: center; text-align: center; padding: 3px; font: 600 clamp(.5rem, .85vw, .68rem)/1.1 var(--font); white-space: pre-line; overflow-wrap: anywhere; cursor: pointer; border: 2px solid transparent; } .key { aspect-ratio: 1; min-width: 0; overflow: hidden; border-radius: 6px; display: flex; align-items: center; justify-content: center; text-align: center; padding: 3px; font: 600 clamp(.5rem, .85vw, .68rem)/1.1 var(--font); white-space: pre-line; overflow-wrap: anywhere; cursor: pointer; border: 2px solid transparent; }
.key.sel { border-color: #fff; } .key.empty { background: #2b2f2d; cursor: default; opacity: .5; } .key.nav { background: #37474f; color: #fff; cursor: default; } .key.sel { border-color: #fff; } .key.empty { background: #2b2f2d; cursor: default; opacity: .5; } .key.nav { background: #37474f; color: #fff; cursor: default; }
.key[draggable="true"] { cursor: grab; } .key.empty[data-drop] { outline: 1px dashed #555; opacity: .7; } .key.empty.over { outline: 2px solid var(--accent); opacity: 1; }
@keyframes blink { 0%, 49% { background: var(--b1); color: var(--f1); } 50%, 100% { background: var(--b2); color: var(--f2); } } @keyframes blink { 0%, 49% { background: var(--b1); color: var(--f1); } 50%, 100% { background: var(--b2); color: var(--f2); } }
.key.blink { animation: blink 1s steps(1) infinite; } @media (prefers-reduced-motion: reduce) { .key.blink { animation: none; } } .key.blink { animation: blink 1s steps(1) infinite; } @media (prefers-reduced-motion: reduce) { .key.blink { animation: none; } }
.tabs { display: flex; gap: 6px; margin: 8px 0 12px; flex-wrap: wrap; } .tabs button.on { background: var(--accent-soft); border-color: var(--accent); } .tray.over { outline: 2px solid var(--accent); border-radius: 8px; } .tray button[draggable="true"] { cursor: grab; }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; align-items: baseline; } .kv dt { color: var(--muted); } .kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; } .tabs { display: flex; gap: 6px; margin: 6px 0 10px; flex-wrap: wrap; align-items: center; } .tabs button.on { background: var(--accent-soft); border-color: var(--accent); }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 3px 12px; align-items: baseline; margin: 0; } .kv dt { color: var(--muted); font-size: .85rem; } .kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
details { margin: 6px 0; } summary { cursor: pointer; color: var(--muted); } details { margin: 6px 0; } summary { cursor: pointer; color: var(--muted); }
pre.log { background: var(--deck); color: #d6dbe4; padding: 10px 12px; border-radius: 10px; font: .8rem/1.5 var(--mono); overflow: auto; max-height: 70vh; white-space: pre-wrap; } pre.log { background: var(--deck); color: #d6dbe4; padding: 10px 12px; border-radius: 10px; font: .8rem/1.5 var(--mono); overflow: auto; max-height: 70vh; white-space: pre-wrap; }
.field { margin: 6px 0; } .field label { display: block; color: var(--muted); font-size: .85rem; } .field .val { font-family: var(--mono); font-size: .9em; overflow-wrap: anywhere; }
.help { color: var(--muted); font-size: .85rem; } /* fields: a short label with a hint, the input beside it on wide screens */
.searchresults { position: absolute; left: 190px; right: 16px; top: 46px; z-index: 6; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.18); max-height: 50vh; overflow: auto; } .field { display: grid; grid-template-columns: minmax(120px, 190px) minmax(0, 1fr); gap: 4px 10px; align-items: center; margin: 5px 0; }
.searchresults a { display: block; padding: 8px 12px; color: var(--fg); text-decoration: none; border-bottom: 1px solid var(--line); } .searchresults a:hover { background: var(--accent-soft); } .field .fl { color: var(--muted); font-size: .85rem; } .field.block { grid-template-columns: 1fr; }
.field input:not([type=checkbox]), .field select, .field textarea, .dialog input, .dialog select { width: 100%; max-width: 520px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); font: inherit; }
.field input[type=checkbox] { justify-self: start; width: 18px; height: 18px; }
textarea.mono { font-family: var(--mono); font-size: .84rem; }
.field input[disabled], .field select[disabled], .field textarea[disabled] { opacity: .7; border-style: dashed; }
input.hhmm { width: 5.5em; } input.pagenum { width: 4.5em; padding: 3px 5px; font: inherit; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--fg); }
label.inline { display: inline-flex; align-items: center; gap: 5px; margin: 2px 8px 2px 0; font-size: .9rem; white-space: nowrap; }
label.btn, a.btn { display: inline-block; cursor: pointer; border: 1px solid var(--line); background: var(--surface); color: var(--fg); border-radius: 8px; padding: 6px 12px; text-decoration: none; }
.help { color: var(--muted); font-size: .84rem; }
@media (max-width: 760px) { .field { grid-template-columns: 1fr; gap: 2px; } }
/* settings page: sections down the left, one section at a time */
.settings { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 16px; }
.subnav { display: flex; flex-direction: column; gap: 2px; position: sticky; top: 64px; align-self: start; }
.subnav a { padding: 6px 10px; border-radius: 8px; color: var(--fg); text-decoration: none; font-size: .92rem; display: flex; justify-content: space-between; }
.subnav a.active, .subnav a:hover { background: var(--accent-soft); color: var(--accent); }
.subnav .sep { border-top: 1px solid var(--line); margin: 6px 8px; }
#subsel { display: none; }
@media (max-width: 760px) { .settings { grid-template-columns: 1fr; } .subnav { display: none; } #subsel { display: block; width: 100%; margin-bottom: 8px; padding: 7px; font: inherit; border-radius: 8px; border: 1px solid var(--line); background: var(--surface); color: var(--fg); } }
/* the weekly calendar: paint when the panel may act by itself */
.cal { display: grid; grid-template-columns: 38px repeat(7, 1fr); gap: 1px; user-select: none; max-width: 560px; background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.cal .h { background: var(--surface); font-size: .72rem; color: var(--muted); text-align: center; padding: 3px 0; }
.cal .t { background: var(--surface); font-size: .66rem; color: var(--muted); text-align: right; padding-right: 4px; line-height: 9px; height: 9px; }
.cal .c { background: var(--surface); height: 9px; cursor: crosshair; } .cal .c.on { background: var(--accent); }
.cal .c.hour { border-top: 1px solid var(--line); }
.cal.ro .c { cursor: default; }
.calwrap { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; }
.searchresults { position: absolute; right: 16px; left: auto; top: 50px; z-index: 6; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.18); max-height: 50vh; overflow: auto; min-width: 280px; }
.searchresults a { display: block; padding: 7px 12px; color: var(--fg); text-decoration: none; border-bottom: 1px solid var(--line); } .searchresults a:hover { background: var(--accent-soft); }
.dialog { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 9; padding: 16px; } .dialog { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 9; padding: 16px; }
.dialog .box { background: var(--surface); border-radius: 12px; padding: 18px; max-width: 520px; width: 100%; max-height: 92vh; overflow: auto; } .dialog .box { background: var(--surface); border-radius: 12px; padding: 18px; max-width: 520px; width: 100%; max-height: 92vh; overflow: auto; }
.dialog label.inline { white-space: nowrap; } .dialog table td { padding: 6px 6px; } .dialog input, .dialog select { margin: 6px 0; } .dialog h3 { margin: 0 0 6px; } .dialog table td { padding: 5px 6px; }
.dialog input { width: 100%; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); font: inherit; margin: 8px 0; }
.notice { padding: 8px 12px; border-radius: 8px; background: var(--accent-soft); margin: 8px 0; } .notice.grey { background: var(--bg); border: 1px solid var(--line); color: var(--muted); } .notice { padding: 8px 12px; border-radius: 8px; background: var(--accent-soft); margin: 8px 0; } .notice.grey { background: var(--bg); border: 1px solid var(--line); color: var(--muted); }
@media (max-width: 760px) { .notice.problem { background: #fdecea; color: #7a1d16; border: 1px solid var(--problem); }
.shell { grid-template-columns: 1fr; } .menu { flex-direction: row; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--line); }
.menu .divider { border: 0; margin: 0 6px; padding: 8px 0; } main { padding: 14px 16px 40px; } .searchresults { left: 16px; }
.top { flex-wrap: wrap; } #search { order: 3; flex-basis: 100%; }
}
/* step 2: editing */
.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; background: var(--accent); color: #fff; text-decoration: none; font-weight: 600; font-size: .88rem; white-space: nowrap; }
.badge.warn { background: var(--warn); color: #1d2420; }
button.toggle.on { background: var(--accent); color: #fff; }
button.danger { border-color: var(--problem); color: var(--problem); } button.primary.danger { background: var(--problem); color: #fff; }
button.small { padding: 3px 9px; font-size: .85rem; }
label.inline { display: inline-flex; align-items: center; gap: 4px; margin: 2px 10px 2px 0; color: var(--fg); font-size: .92rem; }
label.btn { display: inline-block; cursor: pointer; border: 1px solid var(--line); background: var(--surface); border-radius: 8px; padding: 6px 12px; }
.field input:not([type=checkbox]), .field select, .field textarea { width: 100%; max-width: 520px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); font: inherit; }
.field textarea.mono, textarea.mono { font-family: var(--mono); font-size: .85rem; }
.field input[disabled], .field select[disabled], .field textarea[disabled] { opacity: .75; border-style: dashed; }
.field input.hhmm { width: 5.5em; display: inline-block; }
.window { margin: 4px 0; padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: center; }
ol.checks li { margin: 8px 0; } .checkbox { padding: 6px 10px; margin: 4px 0; border-left: 3px solid var(--line); } ol.checks li { margin: 8px 0; } .checkbox { padding: 6px 10px; margin: 4px 0; border-left: 3px solid var(--line); }
ul.changes li.command { font-family: var(--mono); font-size: .9em; color: var(--problem); } ul.changes li.command { font-family: var(--mono); font-size: .9em; color: var(--problem); }
.notice.problem { background: #fdecea; color: #7a1d16; border: 1px solid var(--problem); }
ol.steps { padding-left: 18px; } ol.steps li { margin: 3px 0; } ol.steps { padding-left: 18px; } ol.steps li { margin: 3px 0; }
tr.sel { background: var(--accent-soft); } details.script textarea { width: 100%; font-size: .8rem; }
/* step 3: layout */ @media (max-width: 760px) {
.key[draggable="true"] { cursor: grab; } .key.empty[data-drop] { outline: 1px dashed #555; opacity: .7; } .key.empty.over { outline: 2px solid var(--accent); opacity: 1; } .top { flex-wrap: wrap; gap: 6px; } .nav { order: 3; width: 100%; justify-content: space-between; margin: 0; } .nav a { padding: 4px 4px; } #search { max-width: none; margin-left: 0; }
.tray.over { outline: 2px solid var(--accent); border-radius: 8px; } .tray button[draggable="true"] { cursor: grab; } main { padding: 12px 14px 40px; } .brand .muted { display: none; }
input.pagenum { width: 4.5em; padding: 2px 4px; font: inherit; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--fg); } }
/* step 5: setup */
a.btn { display: inline-block; border: 1px solid var(--line); background: var(--surface); color: var(--fg); border-radius: 8px; padding: 6px 12px; text-decoration: none; }
details.script textarea { width: 100%; font-size: .8rem; } details.how { display: inline-block; margin: 0 0 0 8px; } details.how summary { display: inline; color: var(--accent); cursor: pointer; }