@chrisdudek/yg 5.4.1 → 5.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -16157,6 +16157,7 @@ async function runFill(graph, opts) {
16157
16157
  }
16158
16158
  const detPairs = unverifiedPairs.filter((p2) => p2.kind === "deterministic");
16159
16159
  const llmPairs = onlyDeterministic ? [] : unverifiedPairs.filter((p2) => p2.kind === "llm");
16160
+ const skippedLlmPairs = onlyDeterministic ? unverifiedPairs.filter((p2) => p2.kind === "llm").length : 0;
16160
16161
  const aspectById = /* @__PURE__ */ new Map();
16161
16162
  for (const a of graph.aspects) aspectById.set(a.id, a);
16162
16163
  const deterministicAspectIds = new Set(
@@ -16174,6 +16175,12 @@ async function runFill(graph, opts) {
16174
16175
  `Filling ${unverifiedPairs.length} unverified pairs across ${nodeSet.size} nodes \u2014 ${detPairs.length} deterministic (no cost), ${reviewerCallBudget} reviewer calls (consensus included)
16175
16176
  `
16176
16177
  );
16178
+ if (skippedLlmPairs > 0) {
16179
+ write(
16180
+ ` Deterministic-only mode \u2014 ${skippedLlmPairs} LLM pair${skippedLlmPairs === 1 ? "" : "s"} will NOT be reviewed this run; run \`yg check --approve\` to review ${skippedLlmPairs === 1 ? "it" : "them"}.
16181
+ `
16182
+ );
16183
+ }
16177
16184
  if (dryRun) {
16178
16185
  const byNode = /* @__PURE__ */ new Map();
16179
16186
  for (const p2 of unverifiedPairs) {
@@ -16214,7 +16221,7 @@ async function runFill(graph, opts) {
16214
16221
  return writeChain;
16215
16222
  };
16216
16223
  const setEntry = async (aspectId, unitKey, entry) => {
16217
- (lock.verdicts[aspectId] ??= {})[unitKey] = entry;
16224
+ (lock.verdicts[aspectId] ??= {})[toPosixPath(unitKey)] = entry;
16218
16225
  await persistLock();
16219
16226
  };
16220
16227
  const blockedNodes = /* @__PURE__ */ new Set();
@@ -16375,7 +16382,14 @@ async function runFill(graph, opts) {
16375
16382
  }
16376
16383
  await garbageCollectAndRewrite(graph, lock, persistLock);
16377
16384
  if (reviewerCallsMade === 0 && infraFailures === 0 && runtimeErrors === 0 && companionRuntimeErrors === 0) {
16378
- write("0 reviewer calls made \u2014 all expected pairs hold valid verdicts\n");
16385
+ if (skippedLlmPairs > 0) {
16386
+ write(
16387
+ `0 reviewer calls made \u2014 deterministic-only mode; ${skippedLlmPairs} LLM pair${skippedLlmPairs === 1 ? "" : "s"} left unverified. Run \`yg check --approve\` to review ${skippedLlmPairs === 1 ? "it" : "them"}.
16388
+ `
16389
+ );
16390
+ } else {
16391
+ write("0 reviewer calls made \u2014 all expected pairs hold valid verdicts\n");
16392
+ }
16379
16393
  }
16380
16394
  if (infraFailures > 0) {
16381
16395
  const providers = [...new Set(infraReport.map((r) => r.provider).filter(Boolean))].join(", ");
@@ -16709,6 +16723,10 @@ function resolveTopValue(raw) {
16709
16723
  if (Number.isNaN(n) || n < 1) return null;
16710
16724
  return n;
16711
16725
  }
16726
+ function nextPointer(next) {
16727
+ const firstLine = next.split("\n")[0];
16728
+ return firstLine.trimEnd().endsWith(":") ? next : firstLine;
16729
+ }
16712
16730
  function residualAfterNext(result) {
16713
16731
  if (!result.suggestedNext?.startsWith("yg check --approve")) return "";
16714
16732
  const errors = result.issues.filter((i) => i.severity === "error");
@@ -16757,7 +16775,7 @@ function formatOutput(result, view = { kind: "full" }, autoFilled = false, emoji
16757
16775
  }
16758
16776
  const firstFiltered = [...filteredErrors, ...filteredWarnings][0];
16759
16777
  if (firstFiltered?.messageData.next) {
16760
- const nextCmd = firstFiltered.messageData.next.split("\n")[0];
16778
+ const nextCmd = nextPointer(firstFiltered.messageData.next);
16761
16779
  sections.push("");
16762
16780
  sections.push(`Next (this group): ${nextCmd}`);
16763
16781
  sections.push("");
@@ -16787,7 +16805,7 @@ function formatOutput(result, view = { kind: "full" }, autoFilled = false, emoji
16787
16805
  }
16788
16806
  }
16789
16807
  if (result.suggestedNext) {
16790
- const nextCmd = result.suggestedNext.split("\n")[0];
16808
+ const nextCmd = nextPointer(result.suggestedNext);
16791
16809
  const residual = view.kind === "full" || view.kind === "details" ? residualAfterNext(result) : "";
16792
16810
  sections.push("");
16793
16811
  sections.push(`Next: ${nextCmd}${residual}`);
@@ -21146,8 +21164,11 @@ cached and FINAL for unchanged inputs. Interrupting is safe \u2014 finished pair
21146
21164
  persist, the next run resumes.
21147
21165
 
21148
21166
  When nothing was unverified, the summary says \`0 reviewer calls made \u2014 all
21149
- expected pairs hold valid verdicts\`. Use \`yg impact\` to predict cost before
21150
- editing.
21167
+ expected pairs hold valid verdicts\`. Under \`--only-deterministic\` the header and
21168
+ summary instead name the LLM pairs left unverified \u2014 they are skipped by design,
21169
+ not reviewed \u2014 and point at a full \`yg check --approve\` to review them, so a
21170
+ deterministic-only run never reads as if it verified everything. Use \`yg impact\`
21171
+ to predict cost before editing.
21151
21172
 
21152
21173
  \`--dry-run\` (with \`--approve\`) is a free cost preview: it runs the same
21153
21174
  structural gate, pair classification, and budget computation, prints the
@@ -23236,16 +23257,6 @@ async function computePortalFreshness(graph, lock) {
23236
23257
  }
23237
23258
 
23238
23259
  // src/portal/derive-node-state.ts
23239
- var PAIR_RANK = {
23240
- refused: 4,
23241
- unverified: 3,
23242
- warning: 2,
23243
- verified: 1,
23244
- "n/a": 0
23245
- };
23246
- function worstPairState(states) {
23247
- return states.reduce((worst, s) => PAIR_RANK[s] > PAIR_RANK[worst] ? s : worst, "verified");
23248
- }
23249
23260
  var STATE_RANK = {
23250
23261
  refused: 4,
23251
23262
  unverified: 3,
@@ -23297,6 +23308,18 @@ function computeNotApplicable(node, graph, effectiveIds) {
23297
23308
  return out;
23298
23309
  }
23299
23310
 
23311
+ // src/portal/derive-pair-state.ts
23312
+ var PAIR_RANK = {
23313
+ refused: 4,
23314
+ unverified: 3,
23315
+ warning: 2,
23316
+ verified: 1,
23317
+ "n/a": 0
23318
+ };
23319
+ function worstPairState(states) {
23320
+ return states.reduce((worst, s) => PAIR_RANK[s] > PAIR_RANK[worst] ? s : worst, "verified");
23321
+ }
23322
+
23300
23323
  // src/portal/derive-nodes.ts
23301
23324
  function displayPairState(rawKind, status) {
23302
23325
  if (rawKind === "verified") return "verified";
@@ -23614,28 +23637,6 @@ function buildTypes(graph) {
23614
23637
  }
23615
23638
 
23616
23639
  // src/portal/derive-rest.ts
23617
- function buildBoundary(input) {
23618
- if (input === null) {
23619
- return { phantom: [], declaredOnly: [], forbiddenType: [], unknown: true };
23620
- }
23621
- return {
23622
- phantom: dedupeEdges(input.phantom),
23623
- declaredOnly: dedupeEdges(input.declaredOnly),
23624
- forbiddenType: dedupeEdges(input.forbiddenType),
23625
- unknown: false
23626
- };
23627
- }
23628
- function dedupeEdges(edges) {
23629
- const seen = /* @__PURE__ */ new Set();
23630
- const out = [];
23631
- for (const e of edges) {
23632
- const key = `${e.source}\0${e.target}`;
23633
- if (seen.has(key)) continue;
23634
- seen.add(key);
23635
- out.push(e);
23636
- }
23637
- return out.sort((a, b) => a.source.localeCompare(b.source, "en") || a.target.localeCompare(b.target, "en"));
23638
- }
23639
23640
  function buildSuppressions(markers) {
23640
23641
  return markers.map((m) => ({
23641
23642
  aspectId: m.aspectId,
@@ -23677,6 +23678,30 @@ function buildWorklist(check) {
23677
23678
  }));
23678
23679
  }
23679
23680
 
23681
+ // src/portal/derive-boundary.ts
23682
+ function buildBoundary(input) {
23683
+ if (input === null) {
23684
+ return { phantom: [], declaredOnly: [], forbiddenType: [], unknown: true };
23685
+ }
23686
+ return {
23687
+ phantom: dedupeEdges(input.phantom),
23688
+ declaredOnly: dedupeEdges(input.declaredOnly),
23689
+ forbiddenType: dedupeEdges(input.forbiddenType),
23690
+ unknown: false
23691
+ };
23692
+ }
23693
+ function dedupeEdges(edges) {
23694
+ const seen = /* @__PURE__ */ new Set();
23695
+ const out = [];
23696
+ for (const e of edges) {
23697
+ const key = `${e.source}\0${e.target}`;
23698
+ if (seen.has(key)) continue;
23699
+ seen.add(key);
23700
+ out.push(e);
23701
+ }
23702
+ return out.sort((a, b) => a.source.localeCompare(b.source, "en") || a.target.localeCompare(b.target, "en"));
23703
+ }
23704
+
23680
23705
  // src/portal/extract.ts
23681
23706
  async function extractPortalData(projectRoot, opts) {
23682
23707
  const graph = await loadPortalGraph(projectRoot);
@@ -23930,6 +23955,78 @@ async function readPortalAsset(relPath) {
23930
23955
  // src/portal/server/server.ts
23931
23956
  import { createServer } from "http";
23932
23957
 
23958
+ // src/portal/server/boot-pages.ts
23959
+ var BOOT_PAGE_CSS = `
23960
+ :root{color-scheme:light dark;--yg-bg:#fcfcfd;--yg-surface:#f9f9fb;--yg-fg:#1c2024;--yg-muted:#60646c;--yg-accent:#0090ff;--yg-border:#cdced6}
23961
+ @media (prefers-color-scheme:dark){:root{--yg-bg:#111113;--yg-surface:#18191b;--yg-fg:#edeef0;--yg-muted:#b0b4ba;--yg-accent:#3b9eff;--yg-border:#43484e}}
23962
+ html,body{height:100%}
23963
+ body{margin:0;background:var(--yg-bg);color:var(--yg-fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;display:grid;place-items:center;padding:24px;box-sizing:border-box}
23964
+ .yg-box{display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center;max-width:56ch}
23965
+ .yg-title{font-weight:600;font-size:16px}
23966
+ .yg-sub{color:var(--yg-muted);font-size:13.5px;max-width:46ch}
23967
+ .yg-sub code,.yg-detail{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
23968
+ .yg-sub code{font-size:12px;background:var(--yg-surface);border:1px solid var(--yg-border);border-radius:4px;padding:1px 5px}`;
23969
+ function renderLoadingShell() {
23970
+ return `<!doctype html>
23971
+ <html lang="en">
23972
+ <head>
23973
+ <meta charset="utf-8" />
23974
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23975
+ <title>Yggdrasil Portal</title>
23976
+ <style>${BOOT_PAGE_CSS}
23977
+ .yg-spinner{width:34px;height:34px;border-radius:50%;border:3px solid var(--yg-border);border-top-color:var(--yg-accent);animation:yg-spin .8s linear infinite}
23978
+ @keyframes yg-spin{to{transform:rotate(360deg)}}
23979
+ @media (prefers-reduced-motion:reduce){.yg-spinner{animation-duration:2.4s}}
23980
+ </style>
23981
+ </head>
23982
+ <body>
23983
+ <div class="yg-box" id="yg-boot">
23984
+ <div class="yg-spinner" role="status" aria-label="Loading"></div>
23985
+ <div class="yg-title">Reading your architecture\u2026</div>
23986
+ <div class="yg-sub">Checking the graph and rendering the portal. This can take a moment on a large project.</div>
23987
+ </div>
23988
+ <script>
23989
+ (function(){
23990
+ fetch('/render' + location.search, { headers: { accept: 'text/html' } })
23991
+ .then(function(r){ return r.text(); })
23992
+ .then(function(html){ document.open(); document.write(html); document.close(); })
23993
+ .catch(function(){
23994
+ var b = document.getElementById('yg-boot');
23995
+ if (b) { b.innerHTML =
23996
+ '<div class="yg-title">Couldn\\u2019t reach the portal</div>' +
23997
+ '<div class="yg-sub">The local portal process may have stopped. Restart it in your terminal, then reload this page.</div>'; }
23998
+ });
23999
+ })();
24000
+ </script>
24001
+ </body>
24002
+ </html>
24003
+ `;
24004
+ }
24005
+ function renderErrorPage(message) {
24006
+ return `<!doctype html>
24007
+ <html lang="en">
24008
+ <head>
24009
+ <meta charset="utf-8" />
24010
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
24011
+ <title>Yggdrasil Portal \u2014 couldn\u2019t load</title>
24012
+ <style>${BOOT_PAGE_CSS}
24013
+ .yg-detail{margin-top:4px;max-width:74ch;text-align:left;white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:var(--yg-muted);background:var(--yg-surface);border:1px solid var(--yg-border);border-radius:8px;padding:12px 14px}
24014
+ </style>
24015
+ </head>
24016
+ <body>
24017
+ <div class="yg-box">
24018
+ <div class="yg-title">The portal couldn\u2019t load your architecture</div>
24019
+ <div class="yg-sub">Something went wrong while reading the graph and building the page. Make sure this is a project with a <code>.yggdrasil/</code> graph, then reload. If it keeps happening, run <code>yg check</code> in your terminal to see the underlying problem.</div>
24020
+ <div class="yg-detail">${escapeHtmlText(message)}</div>
24021
+ </div>
24022
+ </body>
24023
+ </html>
24024
+ `;
24025
+ }
24026
+ function escapeHtmlText(s) {
24027
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24028
+ }
24029
+
23933
24030
  // src/portal/server/page.ts
23934
24031
  async function freshPortalData(projectRoot, writeEnabled) {
23935
24032
  return extractPortalData(projectRoot, { writeEnabled });
@@ -23937,6 +24034,12 @@ async function freshPortalData(projectRoot, writeEnabled) {
23937
24034
  async function renderLivePage(data) {
23938
24035
  return renderPortalPage(data);
23939
24036
  }
24037
+ function loadingShell() {
24038
+ return renderLoadingShell();
24039
+ }
24040
+ function errorPage(message) {
24041
+ return renderErrorPage(message);
24042
+ }
23940
24043
  async function readStaticAsset(relPath) {
23941
24044
  return readPortalAsset(relPath);
23942
24045
  }
@@ -24028,9 +24131,18 @@ async function handleRequest(req, res, config) {
24028
24131
  const pathname = url.pathname;
24029
24132
  try {
24030
24133
  if (method === "GET" && pathname === "/") {
24031
- const data = await freshPortalData(config.projectRoot, config.writeEnabled);
24032
- const html = await renderLivePage(data);
24033
- sendText(res, 200, "text/html; charset=utf-8", html);
24134
+ sendText(res, 200, "text/html; charset=utf-8", loadingShell());
24135
+ return;
24136
+ }
24137
+ if (method === "GET" && pathname === "/render") {
24138
+ try {
24139
+ const data = await freshPortalData(config.projectRoot, config.writeEnabled);
24140
+ const html = await renderLivePage(data);
24141
+ sendText(res, 200, "text/html; charset=utf-8", html);
24142
+ } catch (err) {
24143
+ const message = err instanceof Error ? err.message : String(err);
24144
+ sendText(res, 500, "text/html; charset=utf-8", errorPage(message));
24145
+ }
24034
24146
  return;
24035
24147
  }
24036
24148
  if (method === "GET" && pathname === "/data") {
@@ -0,0 +1,278 @@
1
+ /*
2
+ * Component styles — the count header, the tree, the node panel, the glossary tooltip,
3
+ * and the command-palette overlay.
4
+ *
5
+ * The reusable surface pieces the views compose. Tokens come from tokens.css; the shell
6
+ * frame from shell.css. Calm, dense, document-like; the only saturated color is a state
7
+ * (resolved through the state classes in tokens.css). This file stays focused on the
8
+ * in-stage components and the ⌘K overlay.
9
+ */
10
+
11
+ /* ── Count header + chips ───────────────────────────────────────────────── */
12
+ .stage-header {
13
+ border-bottom: 1px solid var(--border);
14
+ padding-bottom: 16px;
15
+ margin-bottom: 18px;
16
+ }
17
+ .stage-title {
18
+ font-size: 24px;
19
+ line-height: 32px;
20
+ font-weight: 600;
21
+ margin: 0 0 4px;
22
+ letter-spacing: -0.01em;
23
+ }
24
+ .stage-sub {
25
+ color: var(--text-secondary);
26
+ font-size: 12.5px;
27
+ margin: 0 0 16px;
28
+ }
29
+ .count-bar {
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ gap: 8px;
33
+ }
34
+ .count-chip {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ gap: 6px;
38
+ padding: 4px 10px;
39
+ border-radius: 7px;
40
+ border: 1px solid var(--border-subtle);
41
+ background: var(--surface-1);
42
+ font-size: 13px;
43
+ }
44
+ .chip-count {
45
+ font-weight: 600;
46
+ }
47
+ .chip-label {
48
+ color: var(--text-secondary);
49
+ }
50
+
51
+ .stage-intro {
52
+ margin-bottom: 14px;
53
+ }
54
+ .stage-notfound {
55
+ margin: 10px 0 0;
56
+ padding: 8px 12px;
57
+ border: 1px solid var(--border);
58
+ border-left: 3px solid var(--unverified);
59
+ border-radius: 8px;
60
+ background: var(--surface-1);
61
+ color: var(--text-secondary);
62
+ font-size: 13px;
63
+ }
64
+ .stage-vtitle {
65
+ font-size: 16px;
66
+ margin: 0 0 2px;
67
+ }
68
+ .stage-vblurb {
69
+ margin: 0;
70
+ color: var(--text-secondary);
71
+ font-size: 13px;
72
+ }
73
+ .stage-note {
74
+ color: var(--text-muted);
75
+ font-size: 12.5px;
76
+ border: 1px dashed var(--border);
77
+ border-radius: 9px;
78
+ padding: 10px 12px;
79
+ margin: 4px 0 18px;
80
+ }
81
+
82
+ /* ── Tree (virtualized) ─────────────────────────────────────────────────── */
83
+ .tree-filter {
84
+ margin: 4px 0 8px;
85
+ }
86
+ .tree-filter-input {
87
+ width: 100%;
88
+ padding: 7px 11px;
89
+ border: 1px solid var(--border);
90
+ border-radius: 8px;
91
+ background: var(--bg);
92
+ color: var(--text-primary);
93
+ font: inherit;
94
+ font-size: 13px;
95
+ }
96
+ /* "You are here" marker for a row revealed by a palette pick / deep-link. */
97
+ .tree-row-revealed {
98
+ background: var(--surface-3);
99
+ box-shadow: inset 3px 0 0 var(--accent);
100
+ }
101
+ .tree-mount {
102
+ margin: 4px 0 18px;
103
+ }
104
+ .tree-scroller {
105
+ position: relative;
106
+ height: 60vh;
107
+ min-height: 280px;
108
+ overflow: auto;
109
+ border: 1px solid var(--border);
110
+ border-radius: 10px;
111
+ background: var(--surface-1);
112
+ }
113
+ .tree-spacer {
114
+ position: relative;
115
+ width: 100%;
116
+ }
117
+ .tree-window {
118
+ position: absolute;
119
+ top: 0;
120
+ left: 0;
121
+ right: 0;
122
+ will-change: transform;
123
+ }
124
+ .tree-row {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 9px;
128
+ height: 28px;
129
+ padding-right: 12px;
130
+ border-bottom: 1px solid var(--surface-2);
131
+ font-size: 13px;
132
+ cursor: pointer;
133
+ }
134
+ .tree-row:hover {
135
+ background: var(--surface-3);
136
+ }
137
+ .tree-name {
138
+ font-weight: 500;
139
+ }
140
+ .tree-type {
141
+ margin-left: auto;
142
+ color: var(--text-muted);
143
+ font-size: 11px;
144
+ }
145
+
146
+ /* ── Node panel ─────────────────────────────────────────────────────────── */
147
+ .panel-title {
148
+ font-size: 13px;
149
+ margin: 0 0 10px;
150
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
151
+ word-break: break-all;
152
+ }
153
+ .panel-head {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 9px;
157
+ font-size: 13px;
158
+ margin-bottom: 8px;
159
+ }
160
+ .panel-sub {
161
+ color: var(--text-secondary);
162
+ font-size: 12.5px;
163
+ margin: 0;
164
+ }
165
+
166
+ /* ── Glossary term tooltip ──────────────────────────────────────────────── */
167
+ .term {
168
+ border-bottom: 1px dotted var(--border-strong);
169
+ cursor: help;
170
+ }
171
+
172
+ /* ── Command palette overlay ────────────────────────────────────────────── */
173
+ .palette-backdrop {
174
+ position: fixed;
175
+ inset: 0;
176
+ display: flex;
177
+ align-items: flex-start;
178
+ justify-content: center;
179
+ padding-top: 60px;
180
+ background: rgba(0, 0, 0, 0.32);
181
+ z-index: 50;
182
+ }
183
+ .palette-box {
184
+ width: 640px;
185
+ max-width: 92vw;
186
+ background: var(--surface-1);
187
+ border: 1px solid var(--border);
188
+ border-radius: 14px;
189
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04), 0 16px 40px -10px rgba(0, 0, 0, 0.45);
190
+ overflow: hidden;
191
+ }
192
+ .palette-input {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 11px;
196
+ padding: 13px 16px;
197
+ border-bottom: 1px solid var(--border-subtle);
198
+ }
199
+ .palette-search-ic {
200
+ color: var(--text-muted);
201
+ }
202
+ .palette-field {
203
+ flex: 1;
204
+ border: 0;
205
+ background: none;
206
+ color: var(--text-primary);
207
+ font: inherit;
208
+ font-size: 16px;
209
+ }
210
+ .palette-field:focus {
211
+ outline: none;
212
+ }
213
+ .palette-results {
214
+ max-height: 380px;
215
+ overflow: auto;
216
+ padding: 6px;
217
+ }
218
+ .palette-empty {
219
+ padding: 16px;
220
+ color: var(--text-muted);
221
+ font-size: 13px;
222
+ }
223
+ .palette-row {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 11px;
227
+ padding: 8px 12px;
228
+ border-radius: 8px;
229
+ font-size: 13.5px;
230
+ cursor: pointer;
231
+ }
232
+ .palette-row.on {
233
+ background: var(--surface-3);
234
+ }
235
+ .palette-ic {
236
+ width: 18px;
237
+ text-align: center;
238
+ color: var(--text-muted);
239
+ }
240
+ .palette-nm {
241
+ font-weight: 500;
242
+ }
243
+ .palette-sub {
244
+ color: var(--text-muted);
245
+ font-size: 11.5px;
246
+ }
247
+ .palette-kind {
248
+ margin-left: auto;
249
+ font-size: 10px;
250
+ color: var(--text-muted);
251
+ border: 1px solid var(--border-subtle);
252
+ border-radius: 5px;
253
+ padding: 0 6px;
254
+ }
255
+ .palette-foot {
256
+ display: flex;
257
+ gap: 16px;
258
+ padding: 9px 14px;
259
+ border-top: 1px solid var(--border-subtle);
260
+ font-size: 11px;
261
+ color: var(--text-muted);
262
+ }
263
+
264
+ .portal-error {
265
+ color: var(--refused);
266
+ padding: 24px;
267
+ font-size: 14px;
268
+ }
269
+
270
+ @media (prefers-reduced-motion: reduce) {
271
+ *,
272
+ *::before,
273
+ *::after {
274
+ transition: none !important;
275
+ animation: none !important;
276
+ scroll-behavior: auto !important;
277
+ }
278
+ }