create-shibumi 0.2.5 → 0.2.8

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.
@@ -25,7 +25,7 @@ const SERVER_HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
25
25
  const COMMIT = /^[a-f0-9]{40}$/;
26
26
  const SERVER_CLI = "~/.local/bin/shibumi-server";
27
27
  const LATEST_SOURCE = "https://shibumistack.dev/ship/latest.ts";
28
- const CURRENT_SOURCE = "https://shibumistack.dev/ship/v45.ts";
28
+ const CURRENT_SOURCE = "https://shibumistack.dev/ship/v46.ts";
29
29
  let sshControlDirectory: string | undefined;
30
30
  let sshControlTarget: string | undefined;
31
31
 
@@ -1703,13 +1703,13 @@ function portIsBusy(port: number): Promise<boolean> {
1703
1703
  });
1704
1704
  }
1705
1705
 
1706
- export function formatDevStartup(port: number, domain: string, time: string, color = false): string {
1706
+ export function formatDevStartup(port: number, domain: string | undefined, time: string, color = false): string {
1707
1707
  const paint = (code: string, value: string) => color ? `\x1b[${code}m${value}\x1b[0m` : value;
1708
1708
  const row = (label: string, url: string) => `${paint("2", "┃")} ${label.padEnd(8)} ${paint("34", url)}`;
1709
1709
  return [
1710
1710
  `${paint("38;5;208", "渋み")} ship dev`,
1711
1711
  row("Local", `http://localhost:${port}/`),
1712
- row("Remote", `https://${domain}`),
1712
+ ...(domain ? [row("Remote", `https://${domain}`)] : []),
1713
1713
  `${paint("2", time)} starting app dev server...`,
1714
1714
  ].join("\n");
1715
1715
  }
@@ -1719,30 +1719,33 @@ function localTime(date = new Date()): string {
1719
1719
  }
1720
1720
 
1721
1721
  async function runDev(): Promise<void> {
1722
+ // Dev must work on a fresh scaffold, before any server exists: without
1723
+ // setup, fall back to the Shibumi port convention (registered apps get the
1724
+ // first free port above 9000) and skip the Remote row.
1722
1725
  const config = await readConfig();
1723
- if (!config) throw new Error("Shibumi setup is missing.\n\nNext: run bun ship:setup.");
1724
- if (await portIsBusy(config.port)) {
1726
+ const port = config?.port ?? 9000;
1727
+ if (await portIsBusy(port)) {
1725
1728
  const lsof = Bun.which("lsof");
1726
1729
  const fuser = Bun.which("fuser");
1727
- if (!lsof && !fuser) throw new Error(`Port ${config.port} is already in use.\n\nNext: stop that process, then run bun dev again.`);
1730
+ if (!lsof && !fuser) throw new Error(`Port ${port} is already in use.\n\nNext: stop that process, then run bun dev again.`);
1728
1731
  const found = lsof
1729
- ? await run([lsof, "-nP", `-iTCP:${config.port}`, "-sTCP:LISTEN", "-t"], { allowFailure: true })
1730
- : await run([fuser!, "-n", "tcp", String(config.port)], { allowFailure: true });
1732
+ ? await run([lsof, "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { allowFailure: true })
1733
+ : await run([fuser!, "-n", "tcp", String(port)], { allowFailure: true });
1731
1734
  const pids = [...new Set(`${found.stdout} ${found.stderr}`.split(/\s+/).filter((value) => /^\d+$/.test(value)).map(Number))];
1732
- if (pids.length === 0) throw new Error(`Port ${config.port} is already in use.\n\nNext: stop that process, then run bun dev again.`);
1735
+ if (pids.length === 0) throw new Error(`Port ${port} is already in use.\n\nNext: stop that process, then run bun dev again.`);
1733
1736
  const details = await run(["ps", "-o", "pid=,comm=", "-p", pids.join(",")], { allowFailure: true });
1734
- log.warn(`Port ${config.port} is in use${details.stdout.trim() ? `:\n${details.stdout.trim()}` : ""}`);
1737
+ log.warn(`Port ${port} is in use${details.stdout.trim() ? `:\n${details.stdout.trim()}` : ""}`);
1735
1738
  const accepted = await confirm({ message: "Stop it and start this project?", initialValue: false });
1736
1739
  if (isCancel(accepted) || !accepted) return;
1737
1740
  for (const pid of pids) process.kill(pid, "SIGTERM");
1738
1741
  const deadline = Date.now() + 5_000;
1739
- while (await portIsBusy(config.port) && Date.now() < deadline) await Bun.sleep(100);
1740
- if (await portIsBusy(config.port)) throw new Error(`Port ${config.port} did not stop.\n\nNext: stop PID ${pids.join(", ")} manually, then run bun dev again.`);
1742
+ while (await portIsBusy(port) && Date.now() < deadline) await Bun.sleep(100);
1743
+ if (await portIsBusy(port)) throw new Error(`Port ${port} did not stop.\n\nNext: stop PID ${pids.join(", ")} manually, then run bun dev again.`);
1741
1744
  }
1742
- process.stdout.write(`${formatDevStartup(config.port, config.domain, localTime(), supportsTerminalColor())}\n`);
1745
+ process.stdout.write(`${formatDevStartup(port, config?.domain, localTime(), supportsTerminalColor())}\n`);
1743
1746
  const child = Bun.spawn([process.execPath, "run", "dev:app"], {
1744
1747
  cwd: root,
1745
- env: { ...process.env, PORT: String(config.port), SHIBUMI_PORT: String(config.port) },
1748
+ env: { ...process.env, PORT: String(port), SHIBUMI_PORT: String(port) },
1746
1749
  stdin: "inherit",
1747
1750
  stdout: "inherit",
1748
1751
  stderr: "inherit",
@@ -1808,7 +1811,7 @@ export async function runShip(): Promise<void> {
1808
1811
  ? await confirm({ message: "Ship now?", initialValue: true })
1809
1812
  : false;
1810
1813
  if (shipNow !== true || isCancel(shipNow)) {
1811
- outro(`${accent("Next:")} ${result.config.trigger === "github-push" ? "git push deploys automatically" : "bun ship"}`);
1814
+ outro(`${accent("Next:")} ${result.config.trigger === "github-push" ? `git push origin ${result.config.branch} to deploy` : "bun ship"}`);
1812
1815
  return;
1813
1816
  }
1814
1817
  }
@@ -1,60 +1,150 @@
1
- :root {
2
- color-scheme: light dark;
3
- --paper: #f5f0e4;
4
- --ink: #1b130f;
5
- --accent: #e95f19;
1
+ /* App styles on top of the vendored shibumi.css tokens (public/vendor/).
2
+ Edit freely; this file is yours. Tokens: --paper --ink --muted --line
3
+ --accent, type scale --text-xs..--text-2xl, --font-sans/serif/mono. */
4
+
5
+ /* The vendored shibumi.css sets a kozo paper background for the sites; apps
6
+ stay flat, and this override also stops the browser fetching the image. */
7
+ body {
8
+ background-image: none;
6
9
  }
7
10
 
8
- @media (prefers-color-scheme: dark) {
9
- :root {
10
- --paper: #1b130f;
11
- --ink: #f5f0e4;
12
- --accent: #ff8648;
13
- }
11
+ .scaffold {
12
+ max-width: 40.625rem;
13
+ margin: 0 auto;
14
+ padding: clamp(3rem, 9vh, 6.5rem) 1.5rem 4rem;
14
15
  }
15
16
 
16
- body {
17
- margin: 0;
18
- background: var(--paper);
19
- color: var(--ink);
20
- font-family: system-ui, sans-serif;
21
- font-size: 1.125rem;
22
- line-height: 1.6;
17
+ .masthead {
18
+ display: flex;
19
+ align-items: baseline;
20
+ gap: 0.6rem;
21
+ margin: 0 0 3.5rem;
22
+ padding-bottom: 1rem;
23
+ border-bottom: 1px solid var(--line);
24
+ color: var(--muted);
25
+ font-size: var(--text-sm);
26
+ letter-spacing: 0.04em;
23
27
  }
24
28
 
25
- main {
26
- max-width: 40rem;
27
- margin: 0 auto;
28
- padding: 4rem 1.5rem;
29
+ .masthead-mark {
30
+ color: var(--accent);
31
+ font-size: var(--text-md);
29
32
  }
30
33
 
31
- h1 {
32
- font-family: Georgia, "Times New Roman", serif;
33
- font-weight: 500;
34
+ .scaffold h1 {
35
+ margin: 0 0 0.75rem;
36
+ font-size: clamp(2.6rem, 7vw, 3.8rem);
37
+ font-weight: 450;
38
+ letter-spacing: -0.03em;
39
+ line-height: 1.05;
34
40
  }
35
41
 
36
- a {
37
- color: inherit;
38
- text-decoration: underline;
39
- text-decoration-color: color-mix(in srgb, var(--accent) 30%, transparent);
40
- text-underline-offset: 0.18em;
42
+ .lede {
43
+ margin: 0 0 3rem;
44
+ max-width: 34rem;
45
+ color: var(--muted);
46
+ font-size: var(--text-lg);
47
+ line-height: 1.55;
41
48
  }
42
49
 
43
- a:hover {
44
- text-decoration-color: var(--accent);
50
+ .demo {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 1rem;
54
+ margin-bottom: 3.25rem;
45
55
  }
46
56
 
47
- button {
48
- font: inherit;
49
- padding: 0.4rem 1rem;
57
+ .demo button {
58
+ font: 500 var(--text-md) var(--font-sans);
59
+ padding: 0.5rem 1.3rem;
50
60
  border: 1px solid var(--ink);
61
+ border-radius: 0.35rem;
51
62
  background: transparent;
52
63
  color: inherit;
53
- border-radius: 4px;
54
64
  cursor: pointer;
65
+ transition: border-color 160ms ease, color 160ms ease;
66
+ }
67
+
68
+ .demo button:hover,
69
+ .demo button:focus-visible {
70
+ border-color: var(--accent);
71
+ color: var(--accent);
55
72
  }
56
73
 
57
- output {
58
- margin-left: 0.75rem;
74
+ .demo output {
75
+ min-width: 2ch;
76
+ font-size: var(--text-lg);
59
77
  font-variant-numeric: tabular-nums;
60
78
  }
79
+
80
+ .demo-hint {
81
+ color: var(--muted);
82
+ font-size: var(--text-sm);
83
+ }
84
+
85
+ .endpoints {
86
+ margin: 0;
87
+ padding: 0;
88
+ list-style: none;
89
+ border-top: 1px solid var(--line);
90
+ }
91
+
92
+ main .endpoints a {
93
+ display: flex;
94
+ align-items: baseline;
95
+ justify-content: space-between;
96
+ gap: 1.5rem;
97
+ padding: 0.9rem 0.25rem;
98
+ border-bottom: 1px solid var(--line);
99
+ text-decoration: none;
100
+ text-decoration-color: transparent;
101
+ transition: background 160ms ease;
102
+ }
103
+
104
+ main .endpoints a:hover,
105
+ main .endpoints a:focus-visible {
106
+ background: var(--faint);
107
+ }
108
+
109
+ .endpoint-path {
110
+ font-family: var(--font-mono);
111
+ font-size: var(--text-sm);
112
+ }
113
+ /* Inline code as rounded chips on the faint layer. */
114
+ .scaffold code {
115
+ padding: 0.12em 0.42em;
116
+ border-radius: 0.35rem;
117
+ background: var(--faint);
118
+ font-family: var(--font-mono);
119
+ font-size: 0.85em;
120
+ }
121
+
122
+
123
+ main .endpoints a:hover .endpoint-path {
124
+ color: var(--accent);
125
+ }
126
+
127
+ .endpoints span {
128
+ color: var(--muted);
129
+ font-size: var(--text-sm);
130
+ text-align: right;
131
+ }
132
+
133
+ .colophon {
134
+ margin-top: 3.5rem;
135
+ }
136
+
137
+ .colophon p {
138
+ margin: 0 0 0.5rem;
139
+ color: var(--muted);
140
+ font-size: var(--text-sm);
141
+ line-height: 1.7;
142
+ }
143
+
144
+ .colophon code {
145
+ color: var(--ink);
146
+ }
147
+
148
+ .colophon-links {
149
+ font-size: var(--text-sm);
150
+ }
@@ -0,0 +1,422 @@
1
+ /* shibumi.css — canonical shared layer for shibumistack.dev, shibumi-server, shibumi-forms.
2
+ Source of truth: shibumistack.dev repo public/shibumi.css. Do not edit vendored copies. */
3
+
4
+ /* shared.css: common styles for all pages */
5
+
6
+ :root {
7
+ color-scheme: light dark;
8
+ /* palette copied from shibumi-server site/styles.css */
9
+ --paper: light-dark(#f5f0e4, #1b130f);
10
+ --ink: light-dark(#272016, #eee5d7);
11
+ --muted: light-dark(#746b5d, #aaa092);
12
+ --faint: light-dark(rgba(39, 32, 22, 0.055), rgba(238, 229, 215, 0.07));
13
+ --line: light-dark(rgba(39, 32, 22, 0.14), rgba(238, 229, 215, 0.16));
14
+ --orange: light-dark(#e95f19, #ff8648);
15
+ --orange-soft: light-dark(#f8dfcd, #3c2117);
16
+ --green: light-dark(#26723e, #75d58a);
17
+ --danger: light-dark(#a13b2c, #e36c59);
18
+ --code: light-dark(#201c16, #120e0b);
19
+ --code-ink: #e9e2d5;
20
+ /* aliases so existing components pick up the server palette */
21
+ --bg: var(--paper);
22
+ --text: var(--ink);
23
+ --quiet: var(--faint);
24
+ --accent: var(--orange);
25
+ --accent-soft: var(--orange-soft);
26
+ --accent-hover: light-dark(#d15515, #ff9a66);
27
+ --success: var(--green);
28
+ --success-text: var(--green);
29
+ --success-ink: #17331f;
30
+ --terminal-success: light-dark(#3d8f50, #78e88f);
31
+ --terminal-active: light-dark(#16839a, #62dff0);
32
+ --terminal-info: light-dark(#397fc0, #74b6ff);
33
+ --max: 72.5rem;
34
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
35
+ --font-serif: ui-serif, Georgia, "Iowan Old Style", "Apple Garamond", Cambria, serif;
36
+ --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
37
+ --text-xs: 0.75rem; /* 12px tiny */
38
+ --text-sm: 0.875rem; /* 14px small */
39
+ --text-md: 1rem; /* 16px normal / UI */
40
+ --text-base: 1.125rem; /* 18px body */
41
+ --text-lg: 1.3125rem; /* 21px */
42
+ --text-xl: 1.563rem; /* 25px */
43
+ --text-2xl: 1.953rem;
44
+ }
45
+
46
+ [data-theme="dark"] { color-scheme: dark; }
47
+ [data-theme="light"] { color-scheme: light; }
48
+
49
+ *, *::before, *::after { box-sizing: border-box; }
50
+ html { min-height: 100%; }
51
+
52
+ body {
53
+ margin: 0;
54
+ min-height: 100svh;
55
+ font-family: var(--font-sans);
56
+ font-size: var(--text-base);
57
+ font-weight: 400;
58
+ line-height: 1.65;
59
+ color: var(--text);
60
+ background: var(--bg);
61
+ }
62
+
63
+ /* Kozo paper texture, light theme only; dark stays flat ink. The image lives
64
+ next to this stylesheet in every consumer, hence the relative URL. */
65
+ body {
66
+ background-image: url("kozo.webp");
67
+ background-size: cover;
68
+ background-position: center;
69
+ background-attachment: fixed;
70
+ background-repeat: no-repeat;
71
+ }
72
+ @media (prefers-color-scheme: dark) {
73
+ body { background-image: none; }
74
+ }
75
+ [data-theme="light"] body { background-image: url("kozo.webp"); }
76
+ [data-theme="dark"] body { background-image: none; }
77
+
78
+ h1, h2, h3 {
79
+ font-family: var(--font-serif);
80
+ }
81
+
82
+ a { color: inherit; }
83
+
84
+ main a:not(.btn):not(.button) {
85
+ text-decoration: underline;
86
+ text-decoration-color: color-mix(in srgb, var(--accent) 30%, transparent);
87
+ text-underline-offset: 0.18em;
88
+ transition: text-decoration-color 160ms ease;
89
+ }
90
+
91
+ main a:not(.btn):not(.button):hover,
92
+ main a:not(.btn):not(.button):focus-visible {
93
+ text-decoration-color: var(--accent);
94
+ }
95
+
96
+ .shell {
97
+ width: min(var(--max), calc(100% - 2rem));
98
+ margin: 0 auto;
99
+ padding: 5.5rem 0 0;
100
+ position: relative;
101
+ }
102
+
103
+ /* ---- header / nav ---- */
104
+
105
+ .shell > header,
106
+ .site-header {
107
+ position: fixed;
108
+ top: 0;
109
+ left: 50%;
110
+ z-index: 20;
111
+ width: min(var(--max), calc(100% - 2rem));
112
+ display: flex;
113
+ justify-content: space-between;
114
+ align-items: center;
115
+ gap: 1rem;
116
+ padding: 1rem 0;
117
+ transform: translateX(-50%);
118
+ }
119
+
120
+ .shell > header::before,
121
+ .site-header::before {
122
+ content: "";
123
+ position: fixed;
124
+ top: 0;
125
+ left: 50%;
126
+ z-index: -1;
127
+ width: 100vw;
128
+ height: 100%;
129
+ transform: translateX(-50%);
130
+ background: light-dark(rgb(245 240 228 / 0%), rgb(27 19 15 / 72%));
131
+ backdrop-filter: blur(1.25rem);
132
+ -webkit-backdrop-filter: blur(1.25rem);
133
+ }
134
+
135
+ .mark {
136
+ display: inline-flex;
137
+ align-items: center;
138
+ gap: 0.75rem;
139
+ color: var(--muted);
140
+ text-decoration: none;
141
+ font-family: var(--font-sans);
142
+ font-size: 1.125rem;
143
+ font-weight: 400;
144
+ letter-spacing: 0.03rem;
145
+ }
146
+
147
+ .mark-tld {
148
+ color: light-dark(rgba(92, 88, 82, 0.5), rgba(189, 183, 173, 0.62));
149
+ font-weight: 300;
150
+ }
151
+
152
+ .mark img,
153
+ img.mark-logo {
154
+ width: 2.125rem;
155
+ height: 2.125rem;
156
+ border-radius: 0.5rem;
157
+ }
158
+
159
+ [data-theme="dark"] .mark .logo-light,
160
+ [data-theme="light"] .mark .logo-dark { display: none; }
161
+
162
+ main, .page { min-width: 0; }
163
+
164
+ .eyebrow {
165
+ margin: 0 0 1rem;
166
+ color: var(--accent);
167
+ font: 300 var(--text-md)/1.2 var(--font-mono);
168
+ letter-spacing: 0.14em;
169
+ text-transform: uppercase;
170
+ }
171
+
172
+ nav {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 1rem;
176
+ font-family: var(--font-sans);
177
+ font-size: var(--text-md);
178
+ color: var(--muted);
179
+ }
180
+
181
+ nav a {
182
+ text-decoration: none;
183
+ border-bottom: 1px solid transparent;
184
+ }
185
+
186
+ nav a:hover,
187
+ nav a[aria-current="page"] {
188
+ border-bottom-color: var(--accent);
189
+ color: var(--text);
190
+ }
191
+
192
+ .nav-links {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 1rem;
196
+ }
197
+
198
+ .nav-actions {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 1rem;
202
+ }
203
+
204
+ .nav-icons {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 0.75rem;
208
+ }
209
+
210
+ .github-link {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ border-bottom: 0;
214
+ color: var(--muted);
215
+ }
216
+
217
+ .github-link svg {
218
+ width: 1.05rem;
219
+ height: 1.05rem;
220
+ fill: currentColor;
221
+ }
222
+
223
+ .github-link:hover {
224
+ color: var(--text);
225
+ border-bottom: 0;
226
+ }
227
+
228
+ .stack-menu { position: relative; }
229
+
230
+ .stack-menu summary {
231
+ cursor: pointer;
232
+ list-style: none;
233
+ transition: color 160ms ease;
234
+ }
235
+
236
+ .stack-menu summary::-webkit-details-marker { display: none; }
237
+ .stack-menu summary:hover,
238
+ .stack-menu summary:focus-visible,
239
+ .stack-menu[open] summary,
240
+ .stack-menu:has(a[aria-current="page"]) summary { color: var(--text); }
241
+
242
+ .stack-menu summary:focus-visible {
243
+ border-radius: 0.2rem;
244
+ outline: 2px solid var(--accent);
245
+ outline-offset: 4px;
246
+ }
247
+
248
+ .stack-popover {
249
+ position: absolute;
250
+ top: calc(100% + 0.75rem);
251
+ right: 0;
252
+ z-index: 30;
253
+ width: 14rem;
254
+ padding: 0.45rem;
255
+ border-radius: 0.5rem;
256
+ background: light-dark(rgb(255 250 240 / 78%), rgb(48 35 29 / 72%));
257
+ -webkit-backdrop-filter: blur(0.5rem);
258
+ backdrop-filter: blur(0.5rem);
259
+ box-shadow: 0 0.8rem 2.5rem light-dark(rgb(20 14 9 / 18%), rgb(0 0 0 / 58%));
260
+ }
261
+
262
+ .stack-popover a {
263
+ display: block;
264
+ padding: 0.65rem 0.75rem;
265
+ border: 0;
266
+ border-radius: 0.3rem;
267
+ color: var(--muted);
268
+ }
269
+
270
+ .stack-popover a:hover,
271
+ .stack-popover a:focus-visible {
272
+ border: 0;
273
+ outline: none;
274
+ background: var(--quiet);
275
+ color: var(--text);
276
+ }
277
+
278
+ .stack-popover a[aria-current="page"] { color: var(--accent); }
279
+
280
+ /* first menu in nav (About) opens aligned to its left edge */
281
+ .nav-links > .stack-menu:first-child .stack-popover { left: 0; right: auto; }
282
+ .stack-popover span,
283
+ .stack-popover small { display: block; }
284
+ .stack-popover span { color: inherit; font-weight: 700; }
285
+ .stack-popover small { margin-top: 0.1rem; color: var(--muted); font-size: var(--text-xs); }
286
+
287
+ /* ---- theme toggle ---- */
288
+
289
+ .theme-toggle {
290
+ appearance: none;
291
+ display: inline-grid;
292
+ place-items: center;
293
+ width: 1.7rem;
294
+ height: 1.7rem;
295
+ border: 0;
296
+ background: transparent;
297
+ color: var(--muted);
298
+ cursor: pointer;
299
+ padding: 0;
300
+ }
301
+
302
+ .theme-toggle:hover { color: var(--text); }
303
+
304
+ .theme-toggle svg {
305
+ width: 1.1rem;
306
+ height: 1.1rem;
307
+ fill: none;
308
+ stroke: currentColor;
309
+ stroke-width: 1.8;
310
+ }
311
+
312
+ [data-theme="dark"] .theme-toggle .icon-sun,
313
+ [data-theme="light"] .theme-toggle .icon-moon { display: none; }
314
+ [data-theme="dark"] .theme-toggle .icon-moon,
315
+ [data-theme="light"] .theme-toggle .icon-sun { display: block; }
316
+ /* alias var names used by server/forms stylesheets */
317
+ :root {
318
+ --sans: var(--font-sans);
319
+ --serif: var(--font-serif);
320
+ --mono: var(--font-mono);
321
+ --fs-xs: var(--text-xs);
322
+ --fs-sm: var(--text-sm);
323
+ --fs-md: var(--text-md);
324
+ --fs-base: var(--text-base);
325
+ --fs-lg: var(--text-lg);
326
+ }
327
+
328
+ /* ---- footer ---- */
329
+
330
+ .site-footer {
331
+ margin-top: clamp(4rem, 8vw, 6rem);
332
+ padding: 1.5rem 0;
333
+ border-top: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
334
+ font-family: var(--font-sans);
335
+ font-size: var(--text-sm);
336
+ color: var(--muted);
337
+ }
338
+
339
+ .footer-line {
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: space-between;
343
+ gap: 1rem;
344
+ }
345
+
346
+ .footer-line nav {
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 1.25rem;
350
+ font: inherit;
351
+ }
352
+
353
+ .site-footer .stack-popover {
354
+ top: auto;
355
+ right: auto;
356
+ bottom: calc(100% + 0.75rem);
357
+ left: 0;
358
+ }
359
+
360
+ .footer-meta {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 0.3rem;
364
+ margin: 0.75rem 0 0;
365
+ font-size: var(--text-xs);
366
+ opacity: 0.75;
367
+ }
368
+
369
+ .site-footer a {
370
+ text-decoration: none;
371
+ color: var(--muted);
372
+ }
373
+
374
+ .site-footer a:hover {
375
+ color: var(--accent);
376
+ }
377
+
378
+ .footer-heart {
379
+ width: 0.875rem;
380
+ height: 0.875rem;
381
+ fill: currentColor;
382
+ color: var(--danger);
383
+ opacity: 0.5;
384
+ transition: opacity 150ms;
385
+ }
386
+
387
+ .footer-meta:hover .footer-heart {
388
+ opacity: 1;
389
+ animation: heart-pulse 1s ease-in-out infinite;
390
+ }
391
+
392
+ @keyframes heart-pulse {
393
+ 0%, 100% { transform: scale(1); }
394
+ 50% { transform: scale(1.3); }
395
+ }
396
+
397
+ .sr-only {
398
+ position: absolute;
399
+ width: 1px;
400
+ height: 1px;
401
+ margin: -1px;
402
+ padding: 0;
403
+ overflow: hidden;
404
+ clip: rect(0 0 0 0);
405
+ white-space: nowrap;
406
+ border: 0;
407
+ }
408
+
409
+
410
+ /* green pulse on the box around a just-used copy button */
411
+ @keyframes copy-glow {
412
+ 0%, 100% { box-shadow: 0 0 0 0 transparent; }
413
+ 35% {
414
+ box-shadow:
415
+ 0 0 0 3px color-mix(in srgb, var(--success) 30%, transparent),
416
+ 0 0 1.5rem color-mix(in srgb, var(--success) 40%, transparent);
417
+ }
418
+ }
419
+
420
+ :has(> .copied), :has(> .is-copied) {
421
+ animation: copy-glow 1.2s ease-in-out;
422
+ }