landfall 0.5.0 → 0.5.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/cli.mjs CHANGED
@@ -815,6 +815,6 @@ Runs on 127.0.0.1 only; the printed link logs the browser in.`,options:{port:{ty
815
815
 
816
816
  Without --packages/--images both are updated. Package upgrades cannot be undone;
817
817
  image updates can (\`lf undo\` puts the old image back). --compose replaces the
818
- compose plugin setup installed with the newest release (undoable too).`,options:{server:F,packages:{type:`boolean`,desc:`upgrade OS packages`},security:{type:`boolean`,desc:`only security updates (apt, dnf/yum); implies --packages`},reboot:{type:`boolean`,desc:`reboot afterwards if the upgrade needs it`},images:{type:`boolean`,desc:`pull newer images and recreate their containers`},compose:{type:`boolean`,desc:`update the compose plugin landfall installed to the newest release`}},async run(e,t){let{name:n,server:r}=H(t.server);if(t.compose){(e.length||t.packages||t.images)&&k(`--compose goes alone`);let i=await hr(r,`updating docker compose`),a=await Nr(i),o=Pr(a);if(!o.composeUpdate)return void(o.composeNote?E(o.composeNote):T(`compose ${a.compose} is the newest`));await N(`Update compose ${a.compose} to ${a.latestCompose} on ${n}?`)&&(w(`downloading docker compose`),await Fr(i),T(`compose updated (lf undo puts the old one back)`));return}let i=!t.packages&&!t.images&&!t.security&&!t.reboot;e.length&&t.packages&&k(`sites only apply to --images`);let a=!e.length&&(t.packages||i||t.security),o=t.images||i||e.length>0;if(a){let e=await hr(r,`upgrading packages`),i=await vr(e),a=t.security?i.updates.filter(e=>e.security):i.updates;i.mgr===`none`?D(`no supported package manager, skipping packages`):a.length?(Io({...i,updates:a},!1),t.security&&(i.mgr===`apk`||i.mgr===`zypper`)&&k(`${i.mgr} has no security-only upgrades here; drop --security`),await N(`Upgrade ${a.length} package${a.length===1?``:`s`} on ${n}?`)&&(w(`upgrading packages on ${n}`),await br(e,{only:t.security?a.map(e=>e.name):void 0,what:t.security?`${a.length} security packages`:`${a.length} packages`,reboot:t.reboot}),T(`packages upgraded`),t.reboot&&E(`rebooting if required; give it a minute`))):T(t.security?`no security updates`:`packages up to date`)}if(o){let t=await Lr(r);if(e.length){let r=e.filter(e=>!t.some(t=>t.name===e));r.length&&k(`no site or service ${r.join(`, `)} on ${n}`)}let i=e.length?t.filter(t=>e.includes(t.name)):t;for(let e of i.filter(e=>e.state!==`update`&&e.state!==`current`&&e.state!==`unknown`))E(C.dim(`${e.name}: ${e.note??e.state}`));let a=i.filter(t=>t.state===`update`||e.includes(t.name)&&t.state===`unknown`);a.length?(Lo(a),await N(`Pull and recreate ${a.map(e=>e.name).join(`, `)}?`)&&(w(`updating images`),await Rr(r,a),T(`images updated (lf undo puts the old ones back)`))):T(`images up to date`)}}});var Vo=`0.5.0`;const Ho=[Eo,No,jo,Li,Ri,zi,qi,Wi,Vi,Hi,Ui,Ki,Gi,Ji,yo,bo,xo,zo,Bo,Xa,Za,ro,ao,lo,to,Qa,eo,no,Oi,ki,ji,Mi,Ni,Ai,Pi,ma,ha,ga,fa,pa,va,_a,Fa,Ia,So,Po],Uo={ls:`sites list`,sites:`sites list`,servers:`server list`,"server ls":`server list`,"server ssh":`ssh`,"sites logs":`logs`,"sites env":`env`,"sites rollback":`rollback`,"sites restart":`restart`,"sites stop":`stop`,"sites start":`start`,"sites ls":`sites list`,"sites remove":`sites rm`,"server remove":`server rm`,uninstall:`server uninstall`,"traefik remove":`traefik rm`,traefik:`traefik status`,firewall:`firewall status`,"firewall remove":`firewall rm`,log:`history`,revert:`undo`,web:`ui`,info:`server info`,"sites domains":`domains`,"sites auth":`auth`,"sites open":`open`,"sites tunnel":`tunnel`,upgrade:`update`},Wo=new Map(Ho.map(e=>[e.name,e]));function Go(e){for(let t of[2,1]){let n=e.slice(0,t).join(` `),r=Wo.get(Uo[n]??n);if(r&&e.length>=t)return{cmd:r,rest:e.slice(t)}}return{rest:e}}function Ko(){let e=[[`Servers`,[`setup`,`server add`,`server list`,`server info`,`server config`,`server domain`,`server use`,`server rm`,`server uninstall`,`ssh`,`keygen`]],[`Traefik`,[`traefik setup`,`traefik status`,`traefik dashboard`,`traefik logs`,`traefik restart`,`traefik rm`]],[`Firewall`,[`firewall status`,`firewall enable`,`firewall disable`,`firewall allow`,`firewall deny`,`firewall update`,`firewall rm`]],[`Sites`,[`deploy`,`sites list`,`sites info`,`domains`,`auth`,`open`,`tunnel`,`logs`,`env`,`rollback`,`restart`,`stop`,`start`,`sites rm`]],[`Updates`,[`updates`,`update`]],[`History`,[`history`,`undo`]],[`Dashboard`,[`ui`]]],t=[`${C.bold(`landfall`)} ${C.dim(Vo)} — set up servers and deploy sites behind Traefik + HTTPS`,``,`${C.bold(`Usage:`)} lf <command> [options] ${C.dim(`lf <command> --help for details`)}`,` lf ui ${C.dim(`web dashboard in your browser`)}`];for(let[n,r]of e){t.push(``,C.bold(n));for(let e of r){let n=Wo.get(e);t.push(` ${(e+(n.args?` `+n.args:``)).padEnd(32)} ${n.summary.split(`
818
+ compose plugin setup installed with the newest release (undoable too).`,options:{server:F,packages:{type:`boolean`,desc:`upgrade OS packages`},security:{type:`boolean`,desc:`only security updates (apt, dnf/yum); implies --packages`},reboot:{type:`boolean`,desc:`reboot afterwards if the upgrade needs it`},images:{type:`boolean`,desc:`pull newer images and recreate their containers`},compose:{type:`boolean`,desc:`update the compose plugin landfall installed to the newest release`}},async run(e,t){let{name:n,server:r}=H(t.server);if(t.compose){(e.length||t.packages||t.images)&&k(`--compose goes alone`);let i=await hr(r,`updating docker compose`),a=await Nr(i),o=Pr(a);if(!o.composeUpdate)return void(o.composeNote?E(o.composeNote):T(`compose ${a.compose} is the newest`));await N(`Update compose ${a.compose} to ${a.latestCompose} on ${n}?`)&&(w(`downloading docker compose`),await Fr(i),T(`compose updated (lf undo puts the old one back)`));return}let i=!t.packages&&!t.images&&!t.security&&!t.reboot;e.length&&t.packages&&k(`sites only apply to --images`);let a=!e.length&&(t.packages||i||t.security),o=t.images||i||e.length>0;if(a){let e=await hr(r,`upgrading packages`),i=await vr(e),a=t.security?i.updates.filter(e=>e.security):i.updates;i.mgr===`none`?D(`no supported package manager, skipping packages`):a.length?(Io({...i,updates:a},!1),t.security&&(i.mgr===`apk`||i.mgr===`zypper`)&&k(`${i.mgr} has no security-only upgrades here; drop --security`),await N(`Upgrade ${a.length} package${a.length===1?``:`s`} on ${n}?`)&&(w(`upgrading packages on ${n}`),await br(e,{only:t.security?a.map(e=>e.name):void 0,what:t.security?`${a.length} security packages`:`${a.length} packages`,reboot:t.reboot}),T(`packages upgraded`),t.reboot&&E(`rebooting if required; give it a minute`))):T(t.security?`no security updates`:`packages up to date`)}if(o){let t=await Lr(r);if(e.length){let r=e.filter(e=>!t.some(t=>t.name===e));r.length&&k(`no site or service ${r.join(`, `)} on ${n}`)}let i=e.length?t.filter(t=>e.includes(t.name)):t;for(let e of i.filter(e=>e.state!==`update`&&e.state!==`current`&&e.state!==`unknown`))E(C.dim(`${e.name}: ${e.note??e.state}`));let a=i.filter(t=>t.state===`update`||e.includes(t.name)&&t.state===`unknown`);a.length?(Lo(a),await N(`Pull and recreate ${a.map(e=>e.name).join(`, `)}?`)&&(w(`updating images`),await Rr(r,a),T(`images updated (lf undo puts the old ones back)`))):T(`images up to date`)}}});var Vo=`0.5.2`;const Ho=[Eo,No,jo,Li,Ri,zi,qi,Wi,Vi,Hi,Ui,Ki,Gi,Ji,yo,bo,xo,zo,Bo,Xa,Za,ro,ao,lo,to,Qa,eo,no,Oi,ki,ji,Mi,Ni,Ai,Pi,ma,ha,ga,fa,pa,va,_a,Fa,Ia,So,Po],Uo={ls:`sites list`,sites:`sites list`,servers:`server list`,"server ls":`server list`,"server ssh":`ssh`,"sites logs":`logs`,"sites env":`env`,"sites rollback":`rollback`,"sites restart":`restart`,"sites stop":`stop`,"sites start":`start`,"sites ls":`sites list`,"sites remove":`sites rm`,"server remove":`server rm`,uninstall:`server uninstall`,"traefik remove":`traefik rm`,traefik:`traefik status`,firewall:`firewall status`,"firewall remove":`firewall rm`,log:`history`,revert:`undo`,web:`ui`,info:`server info`,"sites domains":`domains`,"sites auth":`auth`,"sites open":`open`,"sites tunnel":`tunnel`,upgrade:`update`},Wo=new Map(Ho.map(e=>[e.name,e]));function Go(e){for(let t of[2,1]){let n=e.slice(0,t).join(` `),r=Wo.get(Uo[n]??n);if(r&&e.length>=t)return{cmd:r,rest:e.slice(t)}}return{rest:e}}function Ko(){let e=[[`Servers`,[`setup`,`server add`,`server list`,`server info`,`server config`,`server domain`,`server use`,`server rm`,`server uninstall`,`ssh`,`keygen`]],[`Traefik`,[`traefik setup`,`traefik status`,`traefik dashboard`,`traefik logs`,`traefik restart`,`traefik rm`]],[`Firewall`,[`firewall status`,`firewall enable`,`firewall disable`,`firewall allow`,`firewall deny`,`firewall update`,`firewall rm`]],[`Sites`,[`deploy`,`sites list`,`sites info`,`domains`,`auth`,`open`,`tunnel`,`logs`,`env`,`rollback`,`restart`,`stop`,`start`,`sites rm`]],[`Updates`,[`updates`,`update`]],[`History`,[`history`,`undo`]],[`Dashboard`,[`ui`]]],t=[`${C.bold(`landfall`)} ${C.dim(Vo)} — set up servers and deploy sites behind Traefik + HTTPS`,``,`${C.bold(`Usage:`)} lf <command> [options] ${C.dim(`lf <command> --help for details`)}`,` lf ui ${C.dim(`web dashboard in your browser`)}`];for(let[n,r]of e){t.push(``,C.bold(n));for(let e of r){let n=Wo.get(e);t.push(` ${(e+(n.args?` `+n.args:``)).padEnd(32)} ${n.summary.split(`
819
819
  `)[0]}`)}}t.push(``,C.bold(`Quick start`),` lf setup vps --check ${C.dim(`# what is on vps, what setup would change`)}`,` lf setup vps ${C.dim("# docker + traefik on a server you reach with `ssh vps`")}`,` lf setup box --host 203.0.113.7 ${C.dim("# fresh box (root password once) → `ssh box` works")}`,` lf deploy ./dist -d example.com -s vps ${C.dim(`# publish a static site with HTTPS`)}`,` lf deploy ${C.dim(`# redeploy using ./.landfall.json`)}`,` lf deploy ./dist -y ${C.dim(`# no domain yet: serve at <name>.<ip>.sslip.io`)}`,` lf auth my-site --user anna ${C.dim(`# put a password in front of a site`)}`,` lf updates -s vps && lf update -s vps ${C.dim(`# pending OS/image updates, then apply them`)}`,` lf ls ${C.dim(`# what runs where`)}`,` lf history -s vps && lf undo -s vps ${C.dim(`# what landfall changed, and revert the last of it`)}`,``,C.dim(`Servers are ssh targets: any ~/.ssh/config alias works as --server.`)),console.log(t.join(`
820
820
  `))}async function qo(e){if(!e.length||e[0]===`help`||e[0]===`-h`||e[0]===`--help`)return Ko(),0;if(e[0]===`-v`||e[0]===`--version`||e[0]===`version`)return console.log(Vo),0;let{cmd:t,rest:n}=Go(e);if(!t)return console.error(`${C.red(`error:`)} unknown command "${e.join(` `)}"\n`),Ko(),2;let{values:r,positionals:i,rest:a}=me(t,n);if(r.help)return console.log(he(t)),0;j(!!r.yes),gt.cmd=ge(t,i,r);let o=await t.run(i,r,a);return typeof o==`number`?o:0}async function Jo(e){try{return await qo(e)}catch(e){return e instanceof O?(console.error(`${C.red(`error:`)} ${e.message}`),/--force\b/.test(e.message)&&oe({force:e.message})):console.error(e),1}}Jo(process.argv.slice(2)).then(e=>process.stdout.write(``,()=>process.exit(e)));export{H as A,ye as C,z as D,ve as E,C as M,pe as N,V as O,G as S,Te as T,Ge as _,uo as a,U as b,Ua as c,za as d,Yn as f,at as g,ht as h,po as i,L as j,Be as k,Ba as l,Nn as m,fo as n,mo as o,jn as p,go as r,ho as s,Vo as t,Ra as u,q as v,we as w,W as x,K as y};
package/dist/index.html CHANGED
@@ -9,171 +9,179 @@
9
9
  :root {
10
10
  /* Three text sizes and three corner radii; headings set their own. */
11
11
  --fs-xs: 11.5px; --fs-sm: 12.5px; --fs: 13px;
12
- --r-sm: 6px; --r: 8px; --r-lg: 12px;
13
- --bg: #f6f7f9; --panel: #fff; --line: #e3e6eb; --line-strong: #c4c9d2; --text: #1b1f24; --dim: #5f6774; --faint: #6e7582;
14
- --accent: #1d4ed8; --accent-bg: #e8effd; --accent-solid: var(--accent); --bad-solid: var(--bad); --ok: #13733a; --ok-bg: #e5f5ea; --warn: #b45309; --warn-bg: #fdf3e2;
15
- --bad: #c0262d; --bad-bg: #fbe9ea; --term: #11151b; --term-text: #d7dce3; --shadow: 0 1px 2px rgb(0 0 0 / .05), 0 4px 16px rgb(0 0 0 / .06);
12
+ --r-sm: 7px; --r: 10px; --r-lg: 14px;
13
+ --bg: #fafafa; --panel: #fff; --hover: #f4f4f5; --line: #ececee; --line-strong: #d4d4d8; --text: #18181b; --dim: #5d5d66; --faint: #71717a;
14
+ --accent: #4f46e5; --accent-bg: #eef0fe; --accent-solid: #4f46e5; --bad-solid: #dc2626; --ok: #15803d; --ok-bg: #eaf7ee; --warn: #b45309; --warn-bg: #fdf4e3;
15
+ --bad: #c42323; --bad-bg: #fcecec; --term: #0f0f12; --term-text: #dcdce1;
16
+ --shadow: 0 1px 2px rgb(0 0 0 / .04); --shadow-lg: 0 10px 30px -8px rgb(0 0 0 / .14), 0 2px 6px rgb(0 0 0 / .05);
17
+ --ring: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
16
18
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
17
19
  color-scheme: light;
18
20
  }
19
21
  @media (prefers-color-scheme: dark) {
20
22
  :root {
21
- --bg: #0e1116; --panel: #161a21; --line: #262c36; --line-strong: #3a4250; --text: #e4e7ec; --dim: #8b93a1; --faint: #7d8594;
22
- --accent: #5b8cff; --accent-bg: #1a2540; --accent-solid: #3366dd; --bad-solid: #cc3333; --ok: #4ade80; --ok-bg: #12291b; --warn: #f5b34a; --warn-bg: #2d2412;
23
- --bad: #ff6b6b; --bad-bg: #331719; --term: #0a0d11; --term-text: #d7dce3; --shadow: 0 1px 2px rgb(0 0 0 / .3), 0 6px 20px rgb(0 0 0 / .35);
23
+ --bg: #0c0c0e; --panel: #141417; --hover: #1c1c20; --line: #232328; --line-strong: #34343b; --text: #ececef; --dim: #a1a1aa; --faint: #8b8b94;
24
+ --accent: #8b93ff; --accent-bg: #1d1d3a; --accent-solid: #5b54e8; --bad-solid: #d33a3a; --ok: #4ade80; --ok-bg: #122a1b; --warn: #f5b34a; --warn-bg: #2b2212;
25
+ --bad: #ff7b7b; --bad-bg: #321819; --term: #08080a; --term-text: #dcdce1;
26
+ --shadow: 0 1px 2px rgb(0 0 0 / .4); --shadow-lg: 0 16px 40px -8px rgb(0 0 0 / .6), 0 2px 6px rgb(0 0 0 / .4);
24
27
  color-scheme: dark;
25
28
  }
26
29
  }
27
30
  * { box-sizing: border-box; }
28
31
  html, body { margin: 0; height: 100%; }
29
- body { background: var(--bg); color: var(--text); font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif; }
32
+ body { background: var(--bg); color: var(--text); font: 14px/1.5 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
30
33
  a { color: var(--accent); text-decoration: none; }
31
- a:hover { text-decoration: underline; }
34
+ a:hover { text-decoration: underline; text-underline-offset: 2px; }
32
35
  code, .mono { font-family: var(--mono); font-size: var(--fs-sm); }
33
36
  button {
34
- font: inherit; font-size: var(--fs); border: 1px solid var(--line); background: var(--panel); color: var(--text);
35
- padding: 5px 11px; border-radius: var(--r-sm); cursor: pointer; white-space: nowrap;
37
+ font: inherit; font-size: var(--fs); font-weight: 500; border: 1px solid var(--line-strong); background: var(--panel); color: var(--text);
38
+ padding: 5px 12px; border-radius: var(--r-sm); cursor: pointer; white-space: nowrap; box-shadow: var(--shadow);
39
+ transition: background-color .12s, border-color .12s, color .12s, box-shadow .12s;
36
40
  }
37
- button:hover { border-color: var(--faint); }
38
- button:focus-visible, a:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
39
- button.primary:hover { filter: brightness(1.08); }
41
+ button:hover { background: var(--hover); }
42
+ button:focus-visible, a:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
40
43
  button:disabled { opacity: .5; cursor: default; }
41
44
  button.primary { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; }
45
+ button.primary:hover, button.danger.solid:hover { filter: brightness(1.1); }
42
46
  button.danger { color: var(--bad); }
47
+ button.danger:hover { background: var(--bad-bg); }
43
48
  button.danger.solid { background: var(--bad-solid); border-color: var(--bad-solid); color: #fff; }
44
- button.danger.solid:hover { filter: brightness(1.08); }
45
- button.ghost { border-color: transparent; background: transparent; color: var(--dim); }
46
- button.ghost:hover { color: var(--text); background: var(--bg); }
47
- button.sm { padding: 2px 8px; font-size: var(--fs-sm); }
49
+ button.ghost { border-color: transparent; background: transparent; color: var(--dim); box-shadow: none; }
50
+ button.ghost:hover { color: var(--text); background: var(--hover); }
51
+ button.sm { padding: 2px 9px; font-size: var(--fs-sm); }
48
52
  input, select, textarea {
49
- font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--r-sm); padding: 6px 9px; width: 100%;
53
+ font: inherit; color: var(--text); background: var(--panel); border: 1px solid var(--line-strong); border-radius: var(--r-sm); padding: 7px 10px; width: 100%;
54
+ transition: border-color .12s, box-shadow .12s;
50
55
  }
51
56
  input.secret { -webkit-text-security: disc; text-security: disc; }
52
- input:focus, select:focus, textarea:focus { outline: 3px solid color-mix(in srgb, var(--accent) 30%, transparent); outline-offset: 0; border-color: var(--accent); }
57
+ input:focus, select:focus, textarea:focus { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
53
58
  textarea { font-family: var(--mono); font-size: var(--fs-sm); min-height: 90px; }
54
59
 
55
- .app { display: grid; grid-template-columns: 250px minmax(0, 1fr); grid-template-rows: 52px 1fr; height: 100vh; }
56
- .app.with-job { grid-template-columns: 250px minmax(0, 1fr) clamp(360px, 32vw, 520px); }
57
- header { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; padding: 0 16px; border-bottom: 1px solid var(--line); background: var(--panel); }
58
- .brand { font-weight: 700; font-size: 16px; display: flex; align-items: center; gap: 8px; letter-spacing: -.01em; color: var(--text); }
60
+ .app { display: grid; grid-template-columns: 248px minmax(0, 1fr); grid-template-rows: 56px minmax(0, 1fr); height: 100vh; }
61
+ .app.with-job { grid-template-columns: 248px minmax(0, 1fr) clamp(360px, 32vw, 520px); }
62
+ header { grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; padding: 0 20px; border-bottom: 1px solid var(--line); background: var(--panel); }
63
+ .brand { font-weight: 650; font-size: 15px; display: flex; align-items: center; gap: 9px; letter-spacing: -.015em; color: var(--text); }
59
64
  .brand:hover { text-decoration: none; }
60
- .brand svg { width: 22px; height: 22px; flex: none; }
61
- .brand small { font-weight: 400; color: var(--faint); font-size: var(--fs-sm); letter-spacing: 0; }
65
+ .brand svg { width: 24px; height: 24px; flex: none; }
66
+ .brand small { font-weight: 500; color: var(--faint); font-size: var(--fs-xs); letter-spacing: 0; padding: 1px 7px; border-radius: 99px; background: var(--hover); }
62
67
  .project { margin-left: auto; display: flex; align-items: center; gap: 8px; color: var(--dim); font-size: var(--fs); min-width: 0; }
63
- .project .path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 36vw; }
64
- nav.servers { border-right: 1px solid var(--line); background: var(--panel); overflow: auto; display: flex; flex-direction: column; }
65
- .nav-top { position: sticky; top: 0; z-index: 1; background: var(--panel); display: flex; gap: 6px; padding: 10px 10px 6px; }
66
- .nav-top input { padding: 5px 9px; font-size: var(--fs); }
67
- .nav-top button { flex: none; }
68
- #navlist { padding: 0 8px 12px; }
69
- nav h3 { margin: 10px 8px 4px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--faint); font-weight: 600; display: flex; justify-content: space-between; }
70
- nav h3 span { font-weight: 500; letter-spacing: 0; }
71
- .srv { display: block; width: 100%; text-align: left; border: 0; background: none; padding: 6px 10px; border-radius: var(--r-sm); }
72
- .srv:hover { background: var(--bg); border-color: transparent; }
73
- .srv.on { background: var(--accent-bg); }
74
- .srv.on .n { color: var(--accent); }
75
- .srv.on .t { color: var(--text); }
76
- .srv .n { font-weight: 600; display: flex; align-items: center; gap: 6px; }
77
- .srv .t { color: var(--dim); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
68
+ .project .path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 36vw; font-family: var(--mono); font-size: var(--fs-sm); }
69
+ nav.servers { border-right: 1px solid var(--line); background: var(--bg); overflow: auto; display: flex; flex-direction: column; }
70
+ .nav-top { position: sticky; top: 0; z-index: 1; background: var(--bg); display: flex; gap: 6px; padding: 14px 12px 8px; }
71
+ .nav-top input { padding: 6px 10px; font-size: var(--fs); }
72
+ .nav-top button { flex: none; padding-inline: 10px; }
73
+ #navlist { padding: 0 8px 16px; }
74
+ nav h3 { margin: 14px 10px 4px; font-size: var(--fs-xs); color: var(--faint); font-weight: 500; display: flex; justify-content: space-between; }
75
+ nav h3 span { font-weight: 500; }
76
+ .srv { display: block; width: 100%; text-align: left; border: 0; background: none; box-shadow: none; padding: 6px 10px; border-radius: var(--r-sm); }
77
+ .srv:hover { background: var(--hover); }
78
+ .srv.on { background: var(--panel); box-shadow: 0 0 0 1px var(--line), var(--shadow); }
79
+ .srv.on .n { color: var(--text); }
80
+ .srv.on .t { color: var(--dim); }
81
+ .srv .n { font-weight: 550; display: flex; align-items: center; gap: 6px; }
82
+ .srv .t { color: var(--faint); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
83
  .nav-none { color: var(--faint); font-size: var(--fs-sm); padding: 6px 10px; }
79
84
  .srv .t .live { color: var(--ok); }
80
85
  /* The open server's sections, under it in the list. */
81
- .sections { display: grid; gap: 1px; margin: 2px 0 8px 14px; padding-left: 6px; border-left: 1px solid var(--line); }
82
- .sections button { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; background: none; padding: 4px 10px; color: var(--dim); }
83
- .sections button:hover { background: var(--bg); color: var(--text); }
84
- .sections button.on { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
86
+ .sections { display: grid; gap: 1px; margin: 4px 0 10px 12px; padding-left: 8px; border-left: 1px solid var(--line-strong); }
87
+ .sections button { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; background: none; box-shadow: none; padding: 5px 10px; color: var(--dim); font-weight: 450; }
88
+ .sections button:hover { background: var(--hover); color: var(--text); }
89
+ .sections button.on { background: var(--accent-bg); color: var(--accent); font-weight: 550; }
85
90
  .sections .badge { margin-left: auto; }
86
- .head h1 .crumb { font-weight: 400; color: var(--dim); }
91
+ .head h1 .crumb { font-weight: 400; color: var(--faint); }
87
92
  .tunnel-url { font-family: var(--mono); font-size: var(--fs-sm); }
88
93
  .tunnel-lan::before { content: "· LAN "; color: var(--faint); font-size: var(--fs-sm); }
89
94
  .star { color: var(--warn); font-size: var(--fs-xs); }
90
- main { overflow: auto; padding: 20px 24px 40px; min-width: 0; container-type: inline-size; }
95
+ main { overflow: auto; padding: 28px 32px 48px; min-width: 0; container-type: inline-size; }
91
96
  .show-sm { display: none !important; }
92
97
  /* By main's own width, so an open job panel squeezes the tables like a small screen does. */
93
98
  @container (max-width: 720px) {
94
- td, th { padding: 8px; }
99
+ td, th { padding: 9px 8px; }
95
100
  .hide-sm { display: none; }
96
101
  .show-sm { display: flex !important; }
97
102
  }
98
- .empty { color: var(--dim); text-align: center; padding: 60px 20px; }
99
- .empty h2 { color: var(--text); font-size: 18px; margin: 0 0 6px; }
100
- .head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 20px; margin-bottom: 16px; }
103
+ .empty { color: var(--dim); text-align: center; padding: 72px 20px; }
104
+ .empty h2 { color: var(--text); font-size: 18px; font-weight: 600; letter-spacing: -.01em; margin: 0 0 6px; }
105
+ .head { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px 20px; margin-bottom: 22px; }
101
106
  .head > div:first-child { flex: 1 1 320px; min-width: 0; }
102
- .head h1 { margin: 0; font-size: 22px; }
103
- .head .sub { color: var(--dim); font-size: var(--fs); margin-top: 2px; }
107
+ .head h1 { margin: 0; font-size: 24px; font-weight: 650; letter-spacing: -.02em; line-height: 1.25; }
108
+ .head .sub { color: var(--dim); font-size: var(--fs); margin-top: 4px; }
104
109
  .actions { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }
105
110
  /* Phones only: elsewhere the sections sit under the server in the list. */
106
- .tabs { display: none; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: 16px; overflow-x: auto; scrollbar-width: none; }
111
+ .tabs { display: none; gap: 4px; padding: 4px; background: var(--hover); border-radius: var(--r); margin-bottom: 18px; overflow-x: auto; scrollbar-width: none; }
107
112
  .tabs::-webkit-scrollbar { display: none; }
108
- .tabs button { border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: none; padding: 8px 12px; color: var(--dim); flex: none; }
109
- .tabs button:hover { color: var(--text); }
110
- .tabs button.on { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
111
- .card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow); }
112
- .card + .card { margin-top: 16px; }
113
- .card > h2 { margin: 0; padding: 12px 16px; font-size: 14px; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
113
+ .tabs button { border: 0; border-radius: var(--r-sm); background: none; box-shadow: none; padding: 6px 12px; color: var(--dim); flex: none; }
114
+ .tabs button:hover { color: var(--text); background: none; }
115
+ .tabs button.on { color: var(--text); background: var(--panel); box-shadow: var(--shadow), 0 0 0 1px var(--line); }
116
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow); overflow: clip; }
117
+ .card + .card { margin-top: 20px; }
118
+ .card > h2 { margin: 0; padding: 14px 18px; font-size: 14px; font-weight: 600; border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
114
119
  .card > h2 .actions { font-weight: 400; }
115
120
  .card > h2 .sub { font-weight: 400; color: var(--dim); }
116
- .card > .lede { padding: 10px 16px; color: var(--dim); font-size: var(--fs); border-bottom: 1px solid var(--line); }
121
+ .card > .lede { padding: 10px 18px; color: var(--dim); font-size: var(--fs); border-bottom: 1px solid var(--line); }
117
122
 
118
123
  /* "⋯" menus: one floating list at a time, placed by openMenu(). */
119
- button.more { padding-inline: 7px; letter-spacing: .05em; }
120
- .menu { position: fixed; z-index: 30; min-width: 180px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow); padding: 4px; display: grid; }
121
- .menu button { border: 0; background: none; text-align: left; padding: 6px 10px; border-radius: var(--r-sm); display: flex; justify-content: space-between; gap: 16px; }
122
- .menu button:hover { background: var(--bg); }
123
- .menu button:focus-visible { background: var(--accent-bg); outline: 2px solid var(--accent); outline-offset: -2px; }
124
+ button.more { padding-inline: 8px; letter-spacing: .05em; }
125
+ .menu { position: fixed; z-index: 30; min-width: 190px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); box-shadow: var(--shadow-lg); padding: 5px; display: grid; animation: pop .12s ease-out; }
126
+ .menu button { border: 0; background: none; box-shadow: none; font-weight: 450; text-align: left; padding: 6px 10px; border-radius: var(--r-sm); display: flex; justify-content: space-between; gap: 16px; }
127
+ .menu button:hover { background: var(--hover); }
128
+ .menu button:focus-visible { background: var(--accent-bg); box-shadow: none; }
124
129
  .menu button.danger:hover { background: var(--bad-bg); }
125
130
  .menu button .k { color: var(--faint); font-family: var(--mono); font-size: var(--fs-xs); }
126
131
  .menu hr { border: 0; border-top: 1px solid var(--line); margin: 4px 2px; }
127
-
128
- .home { max-width: 860px; margin: 0 auto; padding-top: 20px; }
129
- .home h1 { margin: 0 0 4px; font-size: 22px; }
130
- .home > p { color: var(--dim); margin: 0 0 20px; }
131
- .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
132
- .tile { display: block; text-align: left; padding: 14px 16px; border-radius: var(--r-lg); background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow); width: 100%; }
133
- .tile:hover { border-color: var(--accent); }
132
+ @keyframes pop { from { opacity: 0; transform: translateY(-3px) scale(.98); } }
133
+
134
+ .home { max-width: 880px; margin: 0 auto; padding-top: 24px; }
135
+ .home h1 { margin: 0 0 6px; font-size: 28px; font-weight: 650; letter-spacing: -.025em; }
136
+ .home > p { color: var(--dim); margin: 0 0 24px; }
137
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
138
+ .tile { display: block; text-align: left; padding: 16px 18px; border-radius: var(--r-lg); background: var(--panel); border: 1px solid var(--line); box-shadow: var(--shadow); width: 100%; transition: border-color .12s, box-shadow .15s, transform .15s; }
139
+ .tile:hover { background: var(--panel); border-color: var(--line-strong); box-shadow: var(--shadow-lg); transform: translateY(-1px); }
134
140
  .tile .n { font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 6px; }
135
- .tile .t { color: var(--dim); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
136
- .tile.add { border-style: dashed; box-shadow: none; color: var(--dim); display: grid; place-items: center; min-height: 64px; }
137
- .cli { margin-top: 28px; padding: 14px 16px; border-radius: var(--r-lg); border: 1px solid var(--line); background: var(--panel); font-size: var(--fs); color: var(--dim); }
138
- .cli pre { margin: 8px 0 0; font: 12.5px/1.6 var(--mono); color: var(--text); white-space: pre-wrap; }
141
+ .tile .t { color: var(--faint); font-size: var(--fs-sm); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
142
+ .tile.add { border: 1.5px dashed var(--line-strong); background: none; box-shadow: none; color: var(--dim); display: grid; place-items: center; min-height: 68px; }
143
+ .tile.add:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); transform: none; box-shadow: none; }
144
+ .cli { margin-top: 32px; padding: 16px 18px; border-radius: var(--r-lg); border: 1px solid var(--line); background: var(--hover); font-size: var(--fs); color: var(--dim); }
145
+ .cli pre { margin: 10px 0 0; font: 12.5px/1.7 var(--mono); color: var(--text); white-space: pre-wrap; }
139
146
  .cli pre span { color: var(--faint); }
140
147
 
141
- .drow { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
148
+ .drow { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 16px; padding: 12px 0; border-bottom: 1px solid var(--line); }
142
149
  .drow:first-child { padding-top: 0; }
150
+ .drow:last-child { border-bottom: 0; padding-bottom: 0; }
143
151
  .drow > div:first-child { flex: 1 1 260px; min-width: 0; }
144
152
  .drow .actions { flex: none; }
145
153
  .site-name { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
146
154
  td .chips.show-sm { margin-top: 4px; }
147
- .card .body { padding: 12px 16px; }
155
+ .card .body { padding: 16px 18px; }
148
156
  table { border-collapse: collapse; width: 100%; }
149
- th { text-align: left; font-weight: 600; font-size: var(--fs-xs); color: var(--faint); text-transform: uppercase; letter-spacing: .04em; padding: 8px 12px; border-bottom: 1px solid var(--line); }
150
- td { padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; }
157
+ th { text-align: left; font-weight: 500; font-size: var(--fs-sm); color: var(--faint); padding: 9px 14px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--hover) 55%, transparent); }
158
+ td { padding: 11px 14px; border-bottom: 1px solid var(--line); vertical-align: middle; }
151
159
  tr:last-child td { border-bottom: 0; }
152
160
  tr.click { cursor: pointer; }
153
- tr.click:hover td { background: var(--bg); }
161
+ tr.click:hover td { background: var(--hover); }
154
162
  td.acts { text-align: right; white-space: nowrap; }
155
163
  .nowrap { white-space: nowrap; }
156
164
  td.acts button { margin-left: 4px; }
157
165
  .scroll { overflow-x: auto; }
158
- .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); margin-right: 6px; vertical-align: 1px; }
159
- .dot.up { background: var(--ok); } .dot.down { background: var(--bad); } .dot.warn { background: var(--warn); }
160
- .badge { display: inline-block; font-size: var(--fs-xs); padding: 1px 7px; border-radius: 99px; background: var(--bg); color: var(--dim); border: 1px solid var(--line); }
166
+ .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); margin-right: 7px; vertical-align: 1px; }
167
+ .dot.up { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-bg); } .dot.down { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-bg); } .dot.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-bg); }
168
+ .badge { display: inline-block; font-size: var(--fs-xs); font-weight: 500; line-height: 1.6; padding: 0 8px; border-radius: 99px; background: var(--hover); color: var(--dim); border: 1px solid var(--line); }
161
169
  .badge.ok { background: var(--ok-bg); color: var(--ok); border-color: transparent; }
162
170
  .badge.warn { background: var(--warn-bg); color: var(--warn); border-color: transparent; }
163
171
  .badge.bad { background: var(--bad-bg); color: var(--bad); border-color: transparent; }
164
172
  .badge.accent { background: var(--accent-bg); color: var(--accent); border-color: transparent; }
165
173
  .dim { color: var(--dim); } .faint { color: var(--faint); }
166
- .note { padding: 10px 14px; border-radius: var(--r); background: var(--warn-bg); color: var(--warn); margin-bottom: 14px; }
174
+ .note { padding: 11px 14px; border-radius: var(--r); background: var(--warn-bg); color: var(--warn); margin-bottom: 16px; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
167
175
  .note.bad { background: var(--bad-bg); color: var(--bad); }
168
176
  .note.info { background: var(--accent-bg); color: var(--accent); }
169
- .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: var(--fs); margin: 0; }
170
- .kv dt { color: var(--dim); } .kv dd { margin: 0; }
171
- pre.block { background: var(--bg); border: 1px solid var(--line); padding: 10px 12px; border-radius: var(--r-sm); overflow-x: auto; font-size: var(--fs-sm); margin: 6px 0 0; }
172
- .detail td { background: var(--bg); }
177
+ .kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; font-size: var(--fs); margin: 0; }
178
+ .kv dt { color: var(--faint); } .kv dd { margin: 0; }
179
+ pre.block { background: var(--hover); border: 1px solid var(--line); padding: 10px 12px; border-radius: var(--r-sm); overflow-x: auto; font-size: var(--fs-sm); margin: 6px 0 0; }
180
+ .detail td { background: color-mix(in srgb, var(--hover) 60%, transparent); }
173
181
  .releases, .services { margin: 0; padding: 0; list-style: none; }
174
- .services li { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
175
- .releases li { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
176
- .run { padding: 12px 16px; border-bottom: 1px solid var(--line); }
182
+ .services li { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
183
+ .releases li { display: flex; align-items: center; gap: 10px; padding: 5px 0; }
184
+ .run { padding: 14px 18px; border-bottom: 1px solid var(--line); }
177
185
  .run:last-child { border-bottom: 0; }
178
186
  .run .top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
179
187
  .run .cmd { font-family: var(--mono); font-weight: 600; font-size: var(--fs); }
@@ -184,49 +192,50 @@ pre.block { background: var(--bg); border: 1px solid var(--line); padding: 10px
184
192
  .steps li.undone .what { text-decoration: line-through; }
185
193
  .steps li button { opacity: 0; }
186
194
  .steps li:hover button, .steps li:focus-within button { opacity: 1; }
187
- .form-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: end; }
195
+ .form-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
188
196
  .form-row label { flex: 1 1 140px; }
189
- label { display: block; font-size: var(--fs-sm); color: var(--dim); }
190
- label > input, label > select, label > textarea { margin-top: 4px; }
191
- .check { display: flex; gap: 8px; align-items: flex-start; color: var(--text); font-size: var(--fs); }
192
- .check input { width: auto; margin-top: 3px; }
193
- .hint { font-size: var(--fs-sm); color: var(--faint); margin-top: 3px; }
194
- .spin { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; vertical-align: -2px; }
197
+ label { display: block; font-size: var(--fs-sm); font-weight: 500; color: var(--dim); }
198
+ label > input, label > select, label > textarea { margin-top: 5px; font-weight: 400; }
199
+ .check { display: flex; gap: 8px; align-items: flex-start; color: var(--text); font-size: var(--fs); font-weight: 400; }
200
+ .check input { width: auto; margin-top: 3px; accent-color: var(--accent-solid); }
201
+ .hint { font-size: var(--fs-sm); font-weight: 400; color: var(--faint); margin-top: 4px; }
202
+ .spin { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--line-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; vertical-align: -2px; }
195
203
  @keyframes spin { to { transform: rotate(360deg); } }
196
- @media (prefers-reduced-motion: reduce) { .spin { animation-duration: 3s; } }
197
- .loading { color: var(--dim); padding: 30px 0; text-align: center; }
204
+ @media (prefers-reduced-motion: reduce) { .spin { animation-duration: 3s; } .menu, dialog[open] { animation: none; } .tile:hover { transform: none; } }
205
+ .loading { color: var(--dim); padding: 40px 0; text-align: center; }
198
206
 
199
207
  [hidden] { display: none !important; }
200
208
  aside.job { border-left: 1px solid var(--line); background: var(--panel); display: flex; flex-direction: column; min-width: 0; }
201
- .job-head { padding: 10px 12px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 8px; }
202
- .job-head .title { font-family: var(--mono); font-size: var(--fs-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
203
- .job-out { flex: 1; overflow: auto; background: var(--term); color: var(--term-text); font: 12.5px/1.5 var(--mono); padding: 10px 12px; white-space: pre-wrap; word-break: break-word; }
204
- .job-out .c-cyan { color: #67d3e8; } .job-out .c-green { color: #6ee7a0; } .job-out .c-yellow { color: #f5c76a; } .job-out .c-red { color: #ff8a8a; } .job-out .c-dim { color: #7b8494; }
205
- .job-foot { padding: 10px 12px; border-top: 1px solid var(--line); display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
209
+ .job-head { padding: 10px 14px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 8px; min-height: 48px; }
210
+ .job-head .title { font-family: var(--mono); font-size: var(--fs-sm); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
211
+ .job-out { flex: 1; overflow: auto; background: var(--term); color: var(--term-text); font: 12.5px/1.6 var(--mono); padding: 12px 14px; white-space: pre-wrap; word-break: break-word; }
212
+ .job-out .c-cyan { color: #67d3e8; } .job-out .c-green { color: #6ee7a0; } .job-out .c-yellow { color: #f5c76a; } .job-out .c-red { color: #ff8a8a; } .job-out .c-dim { color: #7b7b86; }
213
+ .job-foot { padding: 10px 14px; border-top: 1px solid var(--line); display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
206
214
  .job-foot .status { margin-right: auto; font-size: var(--fs); }
207
- .joblist { border-top: 1px solid var(--line); max-height: 28%; overflow: auto; }
208
- .joblist button { display: flex; width: 100%; gap: 8px; border: 0; border-radius: 0; text-align: left; background: none; padding: 6px 12px; font-family: var(--mono); font-size: var(--fs-sm); color: var(--dim); }
215
+ .joblist { border-top: 1px solid var(--line); max-height: 28%; overflow: auto; padding: 4px; }
216
+ .joblist button { display: flex; width: 100%; gap: 8px; border: 0; border-radius: var(--r-sm); box-shadow: none; text-align: left; background: none; padding: 6px 10px; font-family: var(--mono); font-size: var(--fs-sm); font-weight: 400; color: var(--dim); }
209
217
  .joblist button span:last-child { overflow: hidden; text-overflow: ellipsis; }
210
- .joblist button.on { background: var(--accent-bg); color: var(--text); box-shadow: inset 2px 0 0 var(--accent); }
218
+ .joblist button.on { background: var(--accent-bg); color: var(--text); }
211
219
  .joblist .ok { color: var(--ok); } .joblist .bad { color: var(--bad); }
212
220
 
213
- dialog { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0; background: var(--panel); color: var(--text); box-shadow: var(--shadow); width: min(520px, calc(100vw - 32px)); }
214
- dialog::backdrop { background: rgb(0 0 0 / .35); }
215
- dialog form { padding: 18px 20px; display: grid; gap: 12px; }
216
- dialog h2 { margin: 0; font-size: 16px; }
221
+ dialog { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0; background: var(--panel); color: var(--text); box-shadow: var(--shadow-lg); width: min(520px, calc(100vw - 32px)); }
222
+ dialog[open] { animation: pop .15s ease-out; }
223
+ dialog::backdrop { background: rgb(9 9 11 / .4); backdrop-filter: blur(2px); }
224
+ dialog form { padding: 22px 24px; display: grid; gap: 14px; }
225
+ dialog h2 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -.01em; }
217
226
  dialog .text { color: var(--dim); white-space: pre-wrap; font-size: var(--fs); }
218
- dialog .btns { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
219
- fieldset { border: 1px solid var(--line); border-radius: var(--r); padding: 10px 12px; margin: 0; display: grid; gap: 8px; }
227
+ dialog .btns { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
228
+ fieldset { border: 1px solid var(--line); border-radius: var(--r); padding: 12px 14px; margin: 0; display: grid; gap: 10px; }
220
229
  legend { font-size: var(--fs-sm); font-weight: 600; padding: 0 4px; }
221
- .toast { position: fixed; inset: auto auto 16px 50%; transform: translateX(-50%); margin: 0; border: 0; background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: var(--r); font-size: var(--fs); max-width: min(560px, calc(100vw - 32px)); }
222
- .toast button { margin-left: 10px; background: none; border-color: color-mix(in srgb, var(--bg) 40%, transparent); color: inherit; }
230
+ .toast { position: fixed; inset: auto auto 20px 50%; transform: translateX(-50%); margin: 0; border: 0; background: var(--text); color: var(--bg); padding: 10px 16px; border-radius: var(--r); box-shadow: var(--shadow-lg); font-size: var(--fs); font-weight: 500; max-width: min(560px, calc(100vw - 32px)); }
231
+ .toast button { margin-left: 10px; background: none; box-shadow: none; border-color: color-mix(in srgb, var(--bg) 40%, transparent); color: inherit; }
223
232
  .sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
224
- .row-toggle { border: 0; background: none; padding: 2px 4px; margin-left: -4px; display: inline-flex; align-items: center; gap: 4px; color: inherit; font-weight: 600; }
225
- .row-toggle:hover { background: none; text-decoration: underline; }
233
+ .row-toggle { border: 0; background: none; box-shadow: none; padding: 2px 4px; margin-left: -4px; display: inline-flex; align-items: center; gap: 4px; color: inherit; font-weight: 600; }
234
+ .row-toggle:hover { background: none; text-decoration: underline; text-underline-offset: 2px; }
226
235
  .jobs-btn .spin { width: 10px; height: 10px; margin-right: 4px; }
227
236
  .dlg-err { margin: 0; }
228
237
  .server-pick { display: none; }
229
- .facts { display: flex; flex-wrap: wrap; gap: 4px 14px; color: var(--dim); font-size: var(--fs-sm); margin-top: 4px; }
238
+ .facts { display: flex; flex-wrap: wrap; gap: 4px 16px; color: var(--faint); font-size: var(--fs-sm); margin-top: 6px; }
230
239
  .facts b { color: var(--text); font-weight: 500; }
231
240
  .chips { display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
232
241
  .chip { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }
@@ -236,14 +245,14 @@ legend { font-size: var(--fs-sm); font-weight: 600; padding: 0 4px; }
236
245
  .lock { font-size: var(--fs-xs); margin-left: 4px; }
237
246
  dialog.wide { width: min(680px, calc(100vw - 32px)); }
238
247
  .list { display: grid; border: 1px solid var(--line); border-radius: var(--r); max-height: 260px; overflow: auto; }
239
- .list .row { display: flex; gap: 10px; align-items: baseline; border: 0; border-radius: 0; border-bottom: 1px solid var(--line); text-align: left; background: none; padding: 6px 10px; min-width: 0; }
248
+ .list .row { display: flex; gap: 10px; align-items: baseline; border: 0; border-radius: 0; box-shadow: none; font-weight: 400; border-bottom: 1px solid var(--line); text-align: left; background: none; padding: 7px 12px; min-width: 0; }
240
249
  .list .row:last-child { border-bottom: 0; }
241
- .list .row:hover { background: var(--bg); }
250
+ .list .row:hover { background: var(--hover); }
242
251
  .list .row .mono { margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
243
252
  .crumbs { display: flex; flex-wrap: wrap; gap: 2px; align-items: center; font-size: var(--fs-sm); }
244
- .crumbs button { padding: 1px 4px; }
245
- .detected { font-size: var(--fs); padding: 8px 12px; border-radius: var(--r); background: var(--bg); }
246
- .drop { border: 1.5px dashed var(--line); border-radius: var(--r); padding: 12px; text-align: center; color: var(--dim); font-size: var(--fs); }
253
+ .crumbs button { padding: 1px 6px; }
254
+ .detected { font-size: var(--fs); padding: 10px 12px; border-radius: var(--r); background: var(--hover); }
255
+ .drop { border: 1.5px dashed var(--line-strong); border-radius: var(--r); padding: 14px; text-align: center; color: var(--dim); font-size: var(--fs); }
247
256
  .drop label { display: inline; color: var(--accent); cursor: pointer; font-size: var(--fs); }
248
257
  body.dragging::after {
249
258
  content: "Drop a folder or .zip to deploy it"; position: fixed; inset: 12px; z-index: 20; display: grid; place-items: center;
@@ -252,19 +261,20 @@ body.dragging::after {
252
261
 
253
262
  @media (max-width: 1100px) {
254
263
  .app.with-job { grid-template-columns: 220px minmax(0, 1fr); }
255
- aside.job { position: fixed; inset: 52px 0 0 auto; width: min(560px, 100vw); z-index: 5; box-shadow: var(--shadow); }
264
+ aside.job { position: fixed; inset: 56px 0 0 auto; width: min(560px, 100vw); z-index: 5; box-shadow: var(--shadow-lg); }
256
265
  }
257
266
  @media (max-width: 760px) {
258
- .app, .app.with-job { grid-template-columns: minmax(0, 1fr); grid-template-rows: 52px auto 1fr; }
259
- header { padding: 0 12px; gap: 8px; }
267
+ .app, .app.with-job { grid-template-columns: minmax(0, 1fr); grid-template-rows: 56px auto minmax(0, 1fr); }
268
+ header { padding: 0 14px; gap: 8px; }
260
269
  .brand small { display: none; }
261
- .project button { padding: 5px 9px; font-size: var(--fs); white-space: nowrap; }
270
+ .project button { padding: 5px 10px; font-size: var(--fs); white-space: nowrap; }
262
271
  nav.servers { display: none; }
263
- .server-pick { display: block; padding: 10px 16px 0; }
264
- .tabs { display: flex; position: sticky; top: -14px; z-index: 2; background: var(--bg); }
265
- main { padding: 14px 16px 40px; }
272
+ .server-pick { display: block; padding: 12px 16px 0; }
273
+ .tabs { display: flex; position: sticky; top: -14px; z-index: 2; }
274
+ main { padding: 16px 16px 40px; }
275
+ .home h1 { font-size: 24px; }
266
276
  .project .path { display: none; }
267
- aside.job { inset: 52px 0 0 0; width: 100vw; }
277
+ aside.job { inset: 56px 0 0 0; width: 100vw; }
268
278
  td.acts { white-space: normal; }
269
279
  td.acts button { margin: 2px 0 2px 4px; }
270
280
  }
@@ -1012,7 +1022,6 @@ function deployDialog(det, o = {}) {
1012
1022
  if (!n) return invalid("name", "Give the site a name");
1013
1023
  if (!v.server) return invalid("server", "Pick a server");
1014
1024
  const domains = v.domains.split(/[\s,]+/).filter(Boolean);
1015
- if (v.noDomain && domains.length) return invalid("domains", "No domain is ticked below: clear these, or untick it");
1016
1025
  const argv = ["deploy", "-y", ...sv(v.server), "-n", n, ...(v.noDomain ? ["--no-domain"] : domains.flatMap((d) => ["-d", d]))];
1017
1026
  if (v.build?.trim() && v.build.trim() !== p?.build) argv.push("--build", v.build.trim());
1018
1027
  if (v.spa && !p?.spa) argv.push("--spa");
@@ -1030,6 +1039,16 @@ function deployDialog(det, o = {}) {
1030
1039
  run(argv, { server: v.server, cwd: det.path, after: openAfter });
1031
1040
  },
1032
1041
  });
1042
+ noDomainToggle();
1043
+ }
1044
+
1045
+ /** The open dialog's "no domain" box greys out its domains field: ticked, the site gets none. */
1046
+ function noDomainToggle() {
1047
+ const box = $("#f-noDomain"), input = $("#f-domains");
1048
+ if (!box || !input) return;
1049
+ const sync = () => (input.disabled = box.checked);
1050
+ box.addEventListener("change", sync);
1051
+ sync();
1033
1052
  }
1034
1053
 
1035
1054
  /** After a deploy: offer the address it printed. */
@@ -1169,20 +1188,22 @@ function domainsDialog(n, m) {
1169
1188
  text: `Each domain needs an A record pointing at ${ips.join(", ") || "the server"}. Traefik fetches the certificate once it resolves.`,
1170
1189
  fields: [
1171
1190
  { name: "domains", label: "Domains (space separated)", value: m.domains.join(" ") },
1191
+ { name: "noDomain", label: "No domain: reach it through a tunnel only", type: "check", value: !m.domains.length, hint: "Nothing is served on the internet; the site keeps running. Open via tunnel reaches it." },
1172
1192
  ...(main && !m.domains.includes(main) ? [{ name: "apex", label: `Also serve ${main}`, type: "check", value: false, hint: servedByOther(n, m.name, main) ? `${servedByOther(n, m.name, main)} serves it now and hands it over.` : "The main domain itself." }] : []),
1173
1193
  ...(free && !m.domains.includes(free) ? [{ name: "free", label: main ? `Also add ${free}` : `Also add the free address ${free}`, type: "check", value: false, hint: main ? `Under the main domain: works once *.${main} points at the server.` : "Works right away, no DNS setup." }] : []),
1174
1194
  ],
1175
1195
  ok: "Save",
1176
1196
  onOk: (v) => {
1177
- const domains = v.domains.split(/[\s,]+/).filter(Boolean);
1178
- if (v.free) domains.push(free);
1179
- if (v.apex) domains.push(main);
1197
+ const domains = v.noDomain ? [] : v.domains.split(/[\s,]+/).filter(Boolean);
1198
+ if (v.free && !v.noDomain) domains.push(free);
1199
+ if (v.apex && !v.noDomain) domains.push(main);
1180
1200
  // None left: the site keeps running, unrouted.
1181
1201
  const args = domains.length ? domains.flatMap((d) => ["--set", d]) : m.domains.flatMap((d) => ["--rm", d]);
1182
1202
  if (!args.length) return;
1183
1203
  run(["domains", m.name, ...sv(n), ...args, "-y"], { server: n });
1184
1204
  },
1185
1205
  });
1206
+ noDomainToggle();
1186
1207
  }
1187
1208
 
1188
1209
  function authDialog(n, m) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "landfall",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Set up servers and deploy sites behind Traefik with automatic HTTPS",
5
5
  "author": "ph1p <me@ph1p.dev>",
6
6
  "license": "MIT",