slotlock 0.6.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -3,6 +3,38 @@
3
3
  All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
4
4
  versioning.
5
5
 
6
+ ## 0.6.2 - 2026-11-08
7
+
8
+ ### Changed
9
+
10
+ - Dashboard: pages use the width of a wide screen, up to 1600px, and centre past it, instead of
11
+ stopping at 1200px against the sidebar with empty space to their right.
12
+ - SECURITY.md: the hosted service is open, and security research on it is welcome against your own
13
+ account, within the rules it gives (no load testing, no other accounts' data).
14
+
15
+ ## 0.6.1 - 2026-11-08
16
+
17
+ ### Changed
18
+
19
+ - Dashboard: a new look on one set of design tokens, in light and dark. Calmer surfaces (hairline
20
+ borders, no drop shadows), a monospace label scale for table heads and stats, black primary
21
+ buttons, status pills with a dot, and empty states that name the one next step. Long names,
22
+ references, URLs and ids stay on one line with the whole value in a tooltip (and wrap on a
23
+ phone); table dates show the day, with the full UTC time on hover. On a phone the pages sit
24
+ under a top bar with a scrolling tab strip and a sign-out button. A skip link, a live region for
25
+ Copy, Escape and click-outside closing for Rotate, Revoke and Delete (whose button takes focus),
26
+ and reduced motion are respected. The key list names who created a key under its name instead
27
+ of in a column of its own. Routes, form fields, CSRF and origin checks, sessions and the
28
+ Content-Security-Policy are unchanged; the dashboard still loads only its own stylesheet and
29
+ script, and works without script.
30
+
31
+ ### Fixed
32
+
33
+ - Dashboard: Rotate and Revoke on API keys, Delete on webhooks and Remove on the waitlist open
34
+ their confirmation in place, growing the row. It floated below the row inside the table's
35
+ scrolling box, which clipped it: on an account with one key, Revoke opened a button that could
36
+ not be pressed.
37
+
6
38
  ## 0.6.0 - 2026-10-29
7
39
 
8
40
  ### Added
package/README.md CHANGED
@@ -1635,7 +1635,7 @@ Slotlock is availability maths, holds and reservations, events with recurrence,
1635
1635
  free/busy, tenant isolation, iCalendar interchange, MCP and A2A with human confirmation and live
1636
1636
  updates, an MCP Apps view, REST with the application API, scoped API keys, a GitHub sign-in
1637
1637
  dashboard, maintenance, and clients for five languages. [Hosted Slotlock](docs/hosted/README.md)
1638
- is not open; self-hosting is the only supported path today. Provider sync (Google, Microsoft, CalDAV), notifications to people, and a human
1638
+ runs it for you, or you self-host it. Provider sync (Google, Microsoft, CalDAV), notifications to people, and a human
1639
1639
  calendar app are yours to build around it. What each version promises is in
1640
1640
  [VERSIONING.md](VERSIONING.md).
1641
1641
 
package/SECURITY.md CHANGED
@@ -24,10 +24,21 @@ are published as advisories at <https://github.com/PILOTAAI/slotlock/security/ad
24
24
 
25
25
  ## Hosted Slotlock
26
26
 
27
- The hosted service is not open; self-hosting is the only supported path today. Do not test
28
- `slotlock-api.pylota.io` or `app.slotlock.pylota.io`: they are not in scope until this section
29
- says the service is open and gives the rules for testing it. Test Slotlock by running it yourself,
30
- and report what you find as above.
27
+ The hosted service is open: any GitHub account can sign in at
28
+ <https://app.slotlock.pylota.io/dashboard> and use `https://slotlock-api.pylota.io` with its own
29
+ keys. Self-hosting stays fully supported. Security research on the hosted service is welcome within
30
+ these rules:
31
+
32
+ - Test only against an account you signed in with yourself, and only with its own keys. Stop as
33
+ soon as you reach another account's data, keep none of it, and report it.
34
+ - No traffic meant to slow or stop the service: no floods, load tests, or recurrence rules written
35
+ to expand without end. A request over a limit gets `429`; do not try to get around the limits.
36
+ Run Slotlock yourself to load test it.
37
+ - No social engineering, and no attacks on Pylota's people, offices, GitHub, Cloudflare, or other
38
+ providers.
39
+ - Report privately as above, never in a public issue, and give us time to fix it before disclosure.
40
+
41
+ The [Acceptable Use Policy](ACCEPTABLE-USE.md) permits research within these rules. Abuse of the service by others goes to [abuse@pylota.io](mailto:abuse@pylota.io).
31
42
 
32
43
  ## Security model
33
44
 
package/dashboard.js CHANGED
@@ -380,6 +380,8 @@ const ICONS = Object.freeze({
380
380
  assistant: icon('<path d="M4 5h16v11H9l-5 4z"/><path d="M8 9h8M8 12h5"/>'),
381
381
  settings: icon('<path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/>'),
382
382
  check: icon('<path d="M5 12l5 5L20 7"/>'),
383
+ signOut: icon('<path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 17l-5-5 5-5M5 12h11"/>'),
384
+ plus: icon('<path d="M12 5v14M5 12h14"/>'),
383
385
  github: new Html('<svg class="icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path class="fill" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>'),
384
386
  });
385
387
  /** How the overview names each change the feed records. */
@@ -508,6 +510,11 @@ function formatDate(date) {
508
510
  return '—';
509
511
  return `${date.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
510
512
  }
513
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
514
+ /** A table's date: the day (`19 Oct 2026`), with the full UTC time on hover and for machines. */
515
+ function dateCell(date) {
516
+ return html `<time datetime="${date.toISOString()}" title="${formatDate(date)}">${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]} ${date.getUTCFullYear()}</time>`;
517
+ }
511
518
  const WEEKDAY_NAMES = Object.freeze({
512
519
  MO: 'Mon',
513
520
  TU: 'Tue',
@@ -799,9 +806,10 @@ export function createSlotlockDashboard(options) {
799
806
  const name = current?.login ?? session.login;
800
807
  const badge = html `<span class="avatar" aria-hidden="true">${name.slice(0, 1).toUpperCase()}</span>`;
801
808
  if (current && choices.length > 1) {
802
- return html `<form class="workspace" method="post" action="${root}/organization">${badge}<input type="hidden" name="csrf" value="${csrfToken(session)}"><input type="hidden" name="once" value="${formNonce(session)}"><label class="visually-hidden" for="organization">Organization</label><select id="organization" name="organization">${choices.map((org) => html `<option value="${org.id}"${org.id === current.id ? html ` selected` : html ``}>${org.login}</option>`)}</select><button class="quiet" type="submit">Switch</button></form>`;
809
+ return html `<form class="workspace" method="post" action="${root}/organization">${badge}<input type="hidden" name="csrf" value="${csrfToken(session)}"><input type="hidden" name="once" value="${formNonce(session)}"><label class="visually-hidden" for="organization">Organization</label><select id="organization" name="organization" title="${current.login}">${choices.map((org) => html `<option value="${org.id}"${org.id === current.id ? html ` selected` : html ``}>${org.login}</option>`)}</select><button class="quiet" type="submit">Switch</button></form>`;
803
810
  }
804
- return html `<div class="workspace">${badge}<span class="workspace-name">${current ? name : `@${name}`}</span></div>`;
811
+ const shown = current ? name : `@${name}`;
812
+ return html `<div class="workspace">${badge}<span class="workspace-name" title="${shown}">${shown}</span></div>`;
805
813
  }
806
814
  function layout(title, content, session, section) {
807
815
  const head = html `<!doctype html>
@@ -815,31 +823,39 @@ export function createSlotlockDashboard(options) {
815
823
  <script src="${root}/app.js?v=${ASSET_VERSIONS.js}" defer></script>
816
824
  </head>`;
817
825
  const brand = html `<a class="brand" href="${root}">${ICONS.mark}<span class="name">${brandName}</span></a>`;
826
+ const skip = html `<a class="skip" href="#main">Skip to content</a>`;
827
+ // Copy buttons say what they did here, for screen readers (app.js).
828
+ const live = html `<p class="visually-hidden" id="live" aria-live="polite"></p>`;
818
829
  if (!session) {
819
830
  return html `${head}
820
831
  <body class="bare">
832
+ ${skip}
821
833
  <header class="bare-bar">${brand}</header>
822
- <main class="bare-main">${content}</main>
834
+ <main class="bare-main" id="main">${content}</main>
835
+ ${live}
823
836
  </body>
824
837
  </html>`;
825
838
  }
826
839
  const links = nav().map((item) => html `<a href="${root}${item.path}"${item.section === section ? html ` aria-current="page"` : html ``}>${item.icon}<span>${item.label}</span></a>`);
827
840
  return html `${head}
828
841
  <body>
842
+ ${skip}
829
843
  <div class="app">
830
- <aside class="side">
844
+ <header class="side">
831
845
  <div class="side-top">${brand}${workspace(session)}</div>
832
846
  <nav class="nav" aria-label="Dashboard">${links}</nav>
833
- <div class="me"><span class="login">@${session.login}</span><form method="post" action="${root}/sign-out"><input type="hidden" name="csrf" value="${csrfToken(session)}"><button class="quiet" type="submit">Sign out</button></form></div>
834
- </aside>
835
- <main class="page">${content}</main>
847
+ <div class="me"><span class="avatar me-avatar" aria-hidden="true">${session.login.slice(0, 1).toUpperCase()}</span><span class="login" title="@${session.login}">@${session.login}</span><form method="post" action="${root}/sign-out"><input type="hidden" name="csrf" value="${csrfToken(session)}"><button class="quiet sign-out" type="submit" title="Sign out">${ICONS.signOut}<span class="sign-out-label">Sign out</span></button></form></div>
848
+ </header>
849
+ <main class="page" id="main">${content}</main>
836
850
  </div>
851
+ ${live}
837
852
  </body>
838
853
  </html>`;
839
854
  }
840
855
  const noticeOf = (notice) => notice ? html `<p class="notice" role="alert">${notice}</p>` : html ``;
841
856
  function signedOutPage(status, notice, cookies = []) {
842
857
  const content = html `<section class="sign-in">
858
+ <span class="sign-in-mark">${ICONS.mark}</span>
843
859
  <h1>Sign in to ${brandName}</h1>
844
860
  <p>Create keys for your agents, add what they book, and watch what they do.</p>
845
861
  ${noticeOf(notice)}
@@ -861,19 +877,20 @@ ${noticeOf(notice)}
861
877
  <form method="post" action="${root}/keys/revoke"><input type="hidden" name="csrf" value="${token}"><input type="hidden" name="once" value="${formNonce(session)}"><input type="hidden" name="id" value="${key.id}"><button class="danger" type="submit">Revoke key</button></form></div></details>`
862
878
  : html ``;
863
879
  return html `<tr>
864
- <td data-label="Name" class="strong">${key.name}</td>
865
- <td data-label="Key"><code>${key.prefix}…</code></td>
880
+ <td data-label="Name" class="strong"><span class="cell"><span class="truncate" title="${key.name}">${key.name}</span>${served ? html `<span class="meta">Created by ${key.createdBy === personOf(session) ? 'you' : key.createdBy?.startsWith('github:') ? `GitHub user ${key.createdBy.slice(7)}` : 'someone unknown'}</span>` : html ``}</span></td>
881
+ <td data-label="Key"><code class="chip">${key.prefix}…</code></td>
866
882
  <td data-label="Access">${key.scopes.includes('admin') ? 'Full' : key.scopes.includes('write') ? 'Read and write' : 'Read only'}</td>
867
- <td data-label="Created">${formatDate(key.createdAt)}</td>
868
- ${served ? html `<td data-label="Created by">${key.createdBy === personOf(session) ? 'You' : key.createdBy?.startsWith('github:') ? `GitHub user ${key.createdBy.slice(7)}` : 'Unknown'}</td>` : html ``}
869
- <td data-label="Last used">${key.lastUsedAt ? formatDate(key.lastUsedAt) : 'Never'}</td>
870
- <td data-label="Expires">${key.expiresAt ? formatDate(key.expiresAt) : 'Never'}</td>
883
+ <td data-label="Created">${dateCell(key.createdAt)}</td>
884
+ <td data-label="Last used">${key.lastUsedAt ? dateCell(key.lastUsedAt) : html `<span class="muted">Never</span>`}</td>
885
+ <td data-label="Expires">${key.expiresAt ? dateCell(key.expiresAt) : html `<span class="muted">Never</span>`}</td>
871
886
  <td data-label="Status"><span class="pill ${status.toLowerCase()}">${status}</span></td>
872
887
  <td class="actions">${actions}</td>
873
888
  </tr>`;
874
889
  });
875
890
  }
876
891
  const pageHead = (title, action) => html `<header class="page-head"><h1>${title}</h1>${action ?? html ``}</header>`;
892
+ /** A list with nothing in it yet: what goes here, and the one thing to do next. */
893
+ const emptyState = (glyph, text, action) => html `<div class="empty-state"><span class="empty-icon">${glyph}</span><p class="empty">${text}</p>${action ? html `<a class="button" href="${action.href}">${ICONS.plus}${action.label}</a>` : html ``}</div>`;
877
894
  /** A resource as people name it: its reference, else the start of its id. */
878
895
  const resourceName = (resource, id) => resource?.externalRef ?? `${id.slice(0, 8)}…`;
879
896
  function agendaPanel(entries, byId, more) {
@@ -900,7 +917,7 @@ ${served ? html `<td data-label="Created by">${key.createdBy === personOf(sessio
900
917
  const pill = entry.kind === 'hold' && entry.expiresAt
901
918
  ? html `<span class="pill held">Held until ${clock(entry.expiresAt, zone)}</span>`
902
919
  : html `<span class="pill ${entry.kind}">${entry.kind === 'event' ? 'Event' : 'Booked'}</span>`;
903
- group.rows.push(html `<li class="slot"><span class="when">${clock(entry.start, zone)}<span class="until">${clock(entry.end, zone)}</span></span><span class="what"><span class="title">${title}</span>${meta.length > 0 ? html `<span class="meta">${meta.join(' · ')}</span>` : html ``}</span>${pill}</li>`);
920
+ group.rows.push(html `<li class="slot"><span class="when">${clock(entry.start, zone)}<span class="until">${clock(entry.end, zone)}</span></span><span class="what" title="${[title, ...meta].join(' · ')}"><span class="title">${title}</span>${meta.length > 0 ? html `<span class="meta">${meta.join(' · ')}</span>` : html ``}</span>${pill}</li>`);
904
921
  groups.set(day.key, group);
905
922
  }
906
923
  return html `${[...groups.values()].map((group) => html `<h3 class="day">${group.label}</h3><ul class="slots">${group.rows}</ul>`)}${more ? html `<p class="fine">Showing the first ${String(AGENDA_SHOWN)}.</p>` : html ``}`;
@@ -929,7 +946,7 @@ ${served ? html `<td data-label="Created by">${key.createdBy === personOf(sessio
929
946
  const where = change.resourceId
930
947
  ? resourceName(byId.get(change.resourceId), change.resourceId)
931
948
  : null;
932
- return html `<li class="event"><span class="dot ${label.tone}" aria-hidden="true"></span><span class="what"><span class="title">${label.text}${where ? html ` <span class="on">on</span> ${where}` : html ``}</span><span class="meta">${who} · <time datetime="${change.occurredAt.toISOString()}" title="${formatDate(change.occurredAt)}">${ago(change.occurredAt, now())}</time></span></span></li>`;
949
+ return html `<li class="event"><span class="dot ${label.tone}" aria-hidden="true"></span><span class="what" title="${where ? `${label.text} on ${where}` : label.text}"><span class="title">${label.text}${where ? html ` <span class="on">on</span> ${where}` : html ``}</span><span class="meta">${who} · <time datetime="${change.occurredAt.toISOString()}" title="${formatDate(change.occurredAt)}">${ago(change.occurredAt, now())}</time></span></span></li>`;
933
950
  })}</ol>`;
934
951
  }
935
952
  async function overviewPage(session, status = 200, notice) {
@@ -983,23 +1000,20 @@ ${pageHead('API keys')}
983
1000
  <p>Agents call ${brandName} over MCP or A2A with a key: <code>Authorization: Bearer slk_…</code></p>
984
1001
  ${endpoints()}
985
1002
  </section>
986
- <section class="panel">
1003
+ <section class="panel" id="new-key">
987
1004
  <h2>New key</h2>
988
1005
  <form class="create" method="post" action="${root}/keys">
989
1006
  <input type="hidden" name="csrf" value="${token}">
990
1007
  <input type="hidden" name="once" value="${formNonce(session)}">
991
- <label>Name<input name="name" required maxlength="100" placeholder="Booking agent"></label>
1008
+ <label class="grow">Name<input id="key-name" name="name" required maxlength="100" placeholder="Booking agent"></label>
992
1009
  <label>Access<select name="access"><option value="read_write">Read and write</option><option value="read">Read only</option><option value="full">Full, with the application API</option></select></label>
993
1010
  <label>Expires<select name="expires"><option value="90">In 90 days</option><option value="30">In 30 days</option><option value="365">In a year</option><option value="never">Never</option></select></label>
994
1011
  <button class="primary" type="submit">Create key</button>
995
1012
  </form>
996
1013
  </section>
997
1014
  ${keys.length === 0
998
- ? html `<p class="empty">No keys yet. Give every agent its own, with the least access it needs.</p>`
999
- : served
1000
- ? html `<p class="fine">Each key acts in this organization only, and every member can rotate or revoke it.</p><div class="table"><table><thead><tr><th>Name</th><th>Key</th><th>Access</th><th>Created</th><th>Created by</th><th>Last used</th><th>Expires</th><th>Status</th><th><span class="visually-hidden">Actions</span></th></tr></thead>
1001
- <tbody>${keyRows(keys, token, session)}</tbody></table></div>`
1002
- : html `<div class="table"><table><thead><tr><th>Name</th><th>Key</th><th>Access</th><th>Created</th><th>Last used</th><th>Expires</th><th>Status</th><th><span class="visually-hidden">Actions</span></th></tr></thead>
1015
+ ? emptyState(ICONS.keys, 'No keys yet. Give every agent its own, with the least access it needs.', { href: '#key-name', label: 'Create a key' })
1016
+ : html `${served ? html `<p class="fine">Each key acts in this organization only, and every member can rotate or revoke it.</p>` : html ``}<div class="table"><table><thead><tr><th>Name</th><th>Key</th><th>Access</th><th>Created</th><th>Last used</th><th>Expires</th><th>Status</th><th><span class="visually-hidden">Actions</span></th></tr></thead>
1003
1017
  <tbody>${keyRows(keys, token, session)}</tbody></table></div>`}`;
1004
1018
  return respond(status, layout('API keys', content, session, 'keys'));
1005
1019
  }
@@ -1008,20 +1022,20 @@ ${keys.length === 0
1008
1022
  const token = csrfToken(session);
1009
1023
  const content = html `${noticeOf(notice)}
1010
1024
  ${pageHead('Resources', resources.length > 0 ? html `<a class="button" href="${root}/hours">Set hours for every resource</a>` : undefined)}
1011
- <section class="panel">
1025
+ <section class="panel" id="add-resource">
1012
1026
  <h2>Add a resource</h2>
1013
1027
  <form class="create" method="post" action="${root}/resources">
1014
1028
  <input type="hidden" name="csrf" value="${token}">
1015
1029
  <input type="hidden" name="once" value="${formNonce(session)}">
1016
- <label>Reference<input name="reference" required maxlength="200" placeholder="vehicle-42"></label>
1030
+ <label class="grow">Reference<input id="reference" name="reference" required maxlength="200" placeholder="vehicle-42"></label>
1017
1031
  <label>Time zone<input name="timezone" required maxlength="64" value="UTC" placeholder="Europe/London"></label>
1018
1032
  <button class="primary" type="submit">Add resource</button>
1019
1033
  </form>
1020
1034
  <p class="fine">${String(resources.length)} of ${String(SLOTLOCK_DASHBOARD_MAX_RESOURCES)}. Adding a reference you have changes its time zone.</p>
1021
1035
  </section>
1022
1036
  ${resources.length === 0
1023
- ? html `<p class="empty">No resources yet. Agents can only book what is listed here.</p>`
1024
- : html `<div class="table"><table><thead><tr><th>Reference</th><th>Time zone</th><th>Bookable hours</th><th>Resource id</th></tr></thead><tbody>${resources.map((resource) => html `<tr><td data-label="Reference" class="strong"><span class="cell"><a href="${root}/resources/${resource.id}">${resourceName(resource, resource.id)}</a>${resource.pool ? html `<span class="meta">Pool ${resource.pool}</span>` : html ``}</span></td><td data-label="Time zone">${resource.timezone}</td><td data-label="Bookable hours">${hoursSummary(resource.availabilityRules)} <a href="${root}/hours?resource=${resource.id}">Set hours</a></td><td data-label="Resource id"><code class="muted">${resource.id}</code></td></tr>`)}</tbody></table></div>`}`;
1037
+ ? emptyState(ICONS.resources, 'No resources yet. Agents can only book what is listed here.', { href: '#reference', label: 'Add a resource' })
1038
+ : html `<div class="table"><table><thead><tr><th>Reference</th><th>Time zone</th><th>Bookable hours</th><th>Resource id</th></tr></thead><tbody>${resources.map((resource) => html `<tr><td data-label="Reference" class="strong"><span class="cell"><a class="truncate" href="${root}/resources/${resource.id}" title="${resourceName(resource, resource.id)}">${resourceName(resource, resource.id)}</a>${resource.pool ? html `<span class="meta" title="Pool ${resource.pool}">Pool ${resource.pool}</span>` : html ``}</span></td><td data-label="Time zone">${resource.timezone}</td><td data-label="Bookable hours"><span class="cell"><span>${hoursSummary(resource.availabilityRules)}</span><a class="meta-link" href="${root}/hours?resource=${resource.id}">Set hours</a></span></td><td data-label="Resource id"><code class="muted truncate id" title="${resource.id}">${resource.id}</code></td></tr>`)}</tbody></table></div>`}`;
1025
1039
  return respond(status, layout('Resources', content, session, 'resources'));
1026
1040
  }
1027
1041
  async function settingsPage(session, status = 200, notice) {
@@ -1212,10 +1226,10 @@ ${activity ? html `<section class="panel"><h2>Activity</h2>${activityPanel(activ
1212
1226
  ? `Since ${formatDate(endpoint.failingSince)}, ${String(endpoint.attempts)} attempts${endpoint.lastStatus ? `, last HTTP ${endpoint.lastStatus}` : ''}${endpoint.lastError ? `: ${endpoint.lastError.slice(0, 120)}` : ''}`
1213
1227
  : '';
1214
1228
  return html `<tr>
1215
- <td data-label="URL" class="strong wrap"><code>${endpoint.url}</code></td>
1216
- <td data-label="Events">${endpoint.eventTypes ? endpoint.eventTypes.join(', ') : 'All'}</td>
1229
+ <td data-label="URL" class="strong"><code class="truncate url" title="${endpoint.url}">${endpoint.url}</code></td>
1230
+ <td data-label="Events" class="wrap">${endpoint.eventTypes ? endpoint.eventTypes.join(', ') : 'All'}</td>
1217
1231
  <td data-label="Status"><span class="cell"><span class="pill ${state.tone}">${state.text}</span>${problem ? html `<span class="meta wrap">${problem}</span>` : html ``}</span></td>
1218
- <td data-label="Added">${formatDate(endpoint.createdAt)}</td>
1232
+ <td data-label="Added">${dateCell(endpoint.createdAt)}</td>
1219
1233
  <td class="actions">${endpoint.disabledAt
1220
1234
  ? html `<form class="inline" method="post" action="${root}/webhooks/enable"><input type="hidden" name="csrf" value="${token}"><input type="hidden" name="once" value="${formNonce(session)}"><input type="hidden" name="id" value="${endpoint.id}"><button type="submit">Enable</button></form>`
1221
1235
  : html ``}<details class="act danger"><summary>Delete</summary><div class="pop"><p>Nothing more is sent to it. The feed keeps its changes.</p>
@@ -1229,19 +1243,19 @@ ${pageHead('Webhooks')}
1229
1243
  <div class="stat"><span class="stat-label">Failing</span><span class="stat-value">${String(failing.length)}</span></div>
1230
1244
  <div class="stat"><span class="stat-label">Disabled</span><span class="stat-value">${String(disabled.length)}</span></div>
1231
1245
  </div>
1232
- <section class="panel">
1246
+ <section class="panel" id="add-endpoint">
1233
1247
  <h2>Add an endpoint</h2>
1234
1248
  <form method="post" action="${root}/webhooks">
1235
1249
  <input type="hidden" name="csrf" value="${token}">
1236
1250
  <input type="hidden" name="once" value="${formNonce(session)}">
1237
- <label>URL<input name="url" type="url" required maxlength="2048" placeholder="https://example.com/slotlock"></label>
1251
+ <label>URL<input id="webhook-url" name="url" type="url" required maxlength="2048" placeholder="https://example.com/slotlock"></label>
1238
1252
  <details class="types"><summary>Events: all, or choose</summary><div class="checks">${SLOTLOCK_CHANGE_TYPES.map((type) => html `<label class="choice"><input type="checkbox" name="event_type" value="${type}"> <code>${type}</code></label>`)}</div></details>
1239
1253
  <button class="primary" type="submit">Add endpoint</button>
1240
1254
  </form>
1241
1255
  <p class="fine">Signed as Standard Webhooks with a secret shown once. An endpoint failing for a day is disabled.</p>
1242
1256
  </section>
1243
1257
  ${endpoints.length === 0
1244
- ? html `<p class="empty">No webhooks. Add one to hear about bookings as they change.</p>`
1258
+ ? emptyState(ICONS.webhooks, 'No webhooks. Add one to hear about bookings as they change.', { href: '#webhook-url', label: 'Add an endpoint' })
1245
1259
  : html `<div class="table"><table><thead><tr><th>URL</th><th>Events</th><th>Status</th><th>Added</th><th><span class="visually-hidden">Actions</span></th></tr></thead><tbody>${rows}</tbody></table></div>`}`;
1246
1260
  return respond(status, layout('Webhooks', content, session, 'webhooks'));
1247
1261
  }
@@ -1312,9 +1326,9 @@ ${endpoints.length === 0
1312
1326
  const tone = entry.status === 'offered' ? 'active' : entry.status === 'expired' ? 'revoked' : 'held';
1313
1327
  return html `<tr>
1314
1328
  <td data-label="When" class="strong">${day.label} ${clock(entry.start, zone)}–${clock(entry.end, zone)}</td>
1315
- <td data-label="For">${what}</td>
1316
- <td data-label="Holder"><code>${entry.holderRef}</code></td>
1317
- <td data-label="Status"><span class="cell"><span class="pill ${tone}">${entry.status === 'offered' ? 'Offered' : entry.status === 'expired' ? 'Expired' : 'Waiting'}</span>${offered && entry.offeredAt ? html `<span class="meta">${offered}, ${ago(entry.offeredAt, now())}</span>` : html ``}</span></td>
1329
+ <td data-label="For"><span class="truncate" title="${what}">${what}</span></td>
1330
+ <td data-label="Holder"><code class="truncate" title="${entry.holderRef}">${entry.holderRef}</code></td>
1331
+ <td data-label="Status"><span class="cell"><span class="pill ${tone}">${entry.status === 'offered' ? 'Offered' : entry.status === 'expired' ? 'Expired' : 'Waiting'}</span>${offered && entry.offeredAt ? html `<span class="meta" title="${offered}">${offered}, ${ago(entry.offeredAt, now())}</span>` : html ``}</span></td>
1318
1332
  <td data-label="Joined">${ago(entry.createdAt, now())}</td>
1319
1333
  <td class="actions"><details class="act danger"><summary>Remove</summary><div class="pop"><p>${entry.status === 'offered' ? 'Its offered hold is released and offered to the next.' : 'It stops waiting.'}</p>
1320
1334
  <form method="post" action="${root}/waitlist/leave"><input type="hidden" name="csrf" value="${token}"><input type="hidden" name="once" value="${formNonce(session)}"><input type="hidden" name="id" value="${entry.id}"><button class="danger" type="submit">Remove entry</button></form></div></details></td>
@@ -1329,7 +1343,7 @@ ${pageHead('Waitlist')}
1329
1343
  <div class="stat"><span class="stat-label">Offered</span><span class="stat-value">${String(offered)}</span></div>
1330
1344
  </div>
1331
1345
  ${entries.length === 0
1332
- ? html `<p class="empty">Nobody is waiting. Agents add entries when the time they want is taken.</p>`
1346
+ ? emptyState(ICONS.waitlist, 'Nobody is waiting. Agents add entries when the time they want is taken.')
1333
1347
  : html `<div class="table"><table><thead><tr><th>When</th><th>For</th><th>Holder</th><th>Status</th><th>Joined</th><th><span class="visually-hidden">Actions</span></th></tr></thead><tbody>${rows}</tbody></table></div>${entries.length >= WAITLIST_SHOWN ? html `<p class="fine">Showing the first ${String(WAITLIST_SHOWN)}.</p>` : html ``}`}`;
1334
1348
  return respond(status, layout('Waitlist', content, session, 'waitlist'));
1335
1349
  }
@@ -2507,226 +2521,417 @@ ${org ? html `<p>For the organization <strong>${org.login}</strong>.</p>` : html
2507
2521
  },
2508
2522
  };
2509
2523
  }
2510
- // Copies the value of the element a button names. Loaded as a file, so the policy needs no inline code.
2511
- const DASHBOARD_JS = `document.addEventListener('click', (event) => {
2512
- const button = event.target instanceof Element ? event.target.closest('[data-copy]') : null;
2524
+ // Progressive enhancement only, loaded as a file so the policy needs no inline code: copy buttons
2525
+ // (announced in the page's live region), confirmations that close with Escape or a click outside
2526
+ // and put focus on their button, and, on a phone, the current page scrolled into the tab strip.
2527
+ const DASHBOARD_JS = `const live = document.getElementById('live');
2528
+ const say = (text) => {
2529
+ if (!live) return;
2530
+ live.textContent = '';
2531
+ setTimeout(() => { live.textContent = text; }, 50);
2532
+ };
2533
+ document.addEventListener('click', (event) => {
2534
+ const target = event.target instanceof Element ? event.target : null;
2535
+ for (const open of document.querySelectorAll('details.act[open]')) {
2536
+ if (!target || !open.contains(target)) open.removeAttribute('open');
2537
+ }
2538
+ const button = target ? target.closest('[data-copy]') : null;
2513
2539
  if (!button) return;
2514
2540
  const field = document.getElementById(button.getAttribute('data-copy'));
2515
2541
  if (!field) return;
2542
+ const label = button.textContent;
2543
+ const done = (text) => {
2544
+ button.textContent = text;
2545
+ say(text);
2546
+ setTimeout(() => { button.textContent = label; }, 2000);
2547
+ };
2516
2548
  const selectIt = () => {
2517
2549
  if (typeof field.select === 'function') field.select();
2518
- button.textContent = 'Selected: press Ctrl+C or Cmd+C';
2550
+ done('Selected: press Ctrl+C or Cmd+C');
2519
2551
  };
2520
2552
  if (!navigator.clipboard) return selectIt();
2521
- navigator.clipboard.writeText(field.value ?? field.textContent ?? '').then(() => {
2522
- button.textContent = 'Copied';
2523
- }, selectIt);
2553
+ navigator.clipboard.writeText(field.value ?? field.textContent ?? '').then(() => done('Copied'), selectIt);
2554
+ });
2555
+ document.addEventListener('keydown', (event) => {
2556
+ if (event.key !== 'Escape') return;
2557
+ for (const open of document.querySelectorAll('details.act[open]')) {
2558
+ open.removeAttribute('open');
2559
+ const summary = open.querySelector('summary');
2560
+ if (summary) summary.focus();
2561
+ }
2524
2562
  });
2563
+ document.addEventListener('toggle', (event) => {
2564
+ const details = event.target;
2565
+ if (!(details instanceof HTMLDetailsElement) || !details.classList.contains('act') || !details.open) return;
2566
+ const confirm = details.querySelector('.pop button[type=submit]');
2567
+ if (confirm) confirm.focus();
2568
+ }, true);
2569
+ const current = document.querySelector('.nav a[aria-current="page"]');
2570
+ if (current && window.matchMedia('(max-width: 760px)').matches) {
2571
+ current.scrollIntoView({ block: 'nearest', inline: 'center' });
2572
+ }
2525
2573
  `;
2526
2574
  const DASHBOARD_CSS = `:root {
2527
- --ink: #0f1115; --ink-2: #464b57; --ink-3: #7a808d; --line: #e7e8ec; --line-2: #f0f1f4;
2528
- --paper: #ffffff; --wash: #f7f7f9; --side: #fafafb; --hover: #f1f2f5;
2529
- --accent: #2655ff; --accent-wash: #eef2ff; --accent-ink: #ffffff;
2530
- --good: #12805c; --good-wash: #e8f6f0; --bad: #c4321f; --bad-wash: #fdeeec;
2531
- --warn: #8a5a00; --warn-wash: #fff6e0; --held: #8a5a00; --held-wash: #fff4dc;
2532
- --shadow: 0 1px 2px rgba(15, 17, 21, 0.04), 0 1px 1px rgba(15, 17, 21, 0.03);
2533
- color-scheme: light dark;
2575
+ --font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
2576
+ --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
2577
+ --text-xs: 11px; --text-sm: 12.5px; --text-md: 13.5px; --text-lg: 15px; --text-xl: 22px; --text-2xl: 28px;
2578
+ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-12: 48px;
2579
+ --radius-sm: 6px; --radius: 8px; --radius-lg: 12px; --radius-full: 999px;
2580
+ --ease: cubic-bezier(0.2, 0, 0, 1); --fast: 120ms; --slow: 180ms;
2581
+ --canvas: #f7f7f8; --surface: #ffffff; --surface-2: #fafafa; --sunken: #f2f2f4; --hover: #f0f0f2;
2582
+ --line: #e6e6e9; --line-strong: #d4d4d8; --line-soft: #efeff1;
2583
+ --ink: #18181b; --ink-2: #52525b; --ink-3: #6b6b74;
2584
+ --accent: #2456e6; --accent-wash: #eef2fe; --ring: rgba(36, 86, 230, 0.28);
2585
+ --button: #18181b; --button-ink: #ffffff; --button-hover: #2e2e33;
2586
+ --good: #11764c; --good-wash: #e9f6ef; --bad: #c42b1c; --bad-ink: #ffffff; --bad-wash: #fdeeec; --bad-line: #f5c9c3;
2587
+ --warn: #8a5300; --warn-wash: #fff5e1; --warn-line: #f3dcae;
2588
+ --brand: #ff5b2e;
2589
+ --pop-shadow: 0 1px 2px rgba(24, 24, 27, 0.06), 0 12px 32px -8px rgba(24, 24, 27, 0.18);
2590
+ color-scheme: light;
2534
2591
  }
2535
2592
  @media (prefers-color-scheme: dark) {
2536
- :root { --ink: #eceef3; --ink-2: #b9bdc8; --ink-3: #868c99; --line: #262932; --line-2: #1d2027;
2537
- --paper: #0d0f13; --wash: #14171d; --side: #101217; --hover: #1b1e25;
2538
- --accent: #7b93ff; --accent-wash: #1a2145; --accent-ink: #0d0f13;
2539
- --good: #4cc79a; --good-wash: #0f2a20; --bad: #ff7b6b; --bad-wash: #34150f;
2540
- --warn: #ffcf70; --warn-wash: #2b2410; --held: #ffcf70; --held-wash: #2b2410;
2541
- --shadow: none; }
2593
+ :root {
2594
+ --canvas: #0b0b0d; --surface: #111114; --surface-2: #141418; --sunken: #18181c; --hover: #1c1c21;
2595
+ --line: #26262c; --line-strong: #34343b; --line-soft: #1d1d22;
2596
+ --ink: #ededf0; --ink-2: #b4b4bc; --ink-3: #8e8e98;
2597
+ --accent: #7ea2ff; --accent-wash: #18203a; --ring: rgba(126, 162, 255, 0.35);
2598
+ --button: #ededf0; --button-ink: #0b0b0d; --button-hover: #ffffff;
2599
+ --good: #4fca8f; --good-wash: #0f2419; --bad: #ff7a6b; --bad-ink: #1a0806; --bad-wash: #2c1512; --bad-line: #5a2a24;
2600
+ --warn: #f4c25a; --warn-wash: #2a2210; --warn-line: #4d3d17;
2601
+ --pop-shadow: 0 0 0 1px #26262c, 0 16px 40px -8px rgba(0, 0, 0, 0.7);
2602
+ color-scheme: dark;
2603
+ }
2542
2604
  }
2543
- * { box-sizing: border-box; }
2544
- html { -webkit-text-size-adjust: 100%; }
2545
- body { margin: 0; background: var(--paper); color: var(--ink); font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
2546
- a { color: var(--accent); text-decoration: none; }
2605
+ *, *::before, *::after { box-sizing: border-box; }
2606
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
2607
+ body { margin: 0; background: var(--canvas); color: var(--ink); font: 400 14px/1.5 var(--font); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
2608
+ a { color: var(--accent); text-decoration: none; text-underline-offset: 2px; }
2547
2609
  a:hover { text-decoration: underline; }
2548
- code, pre, input[readonly] { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
2610
+ code, pre, kbd, input[readonly] { font-family: var(--mono); font-size: 12.5px; }
2611
+ ::selection { background: var(--ring); }
2612
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
2549
2613
  .icon { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
2550
2614
  .icon .fill { fill: currentColor; stroke: none; }
2551
2615
  .mark { width: 20px; height: 18px; flex: none; }
2552
2616
  .mark-ink { fill: currentColor; }
2553
- .mark-slot { fill: #FF5B2E; }
2554
- .brand { display: inline-flex; align-items: center; gap: 8px; color: var(--ink); font-weight: 650; font-size: 15px; letter-spacing: -0.01em; }
2617
+ .mark-slot { fill: var(--brand); }
2618
+ .skip { position: absolute; left: var(--space-3); top: -48px; z-index: 10; padding: var(--space-2) var(--space-3); border-radius: var(--radius); background: var(--button); color: var(--button-ink); font-weight: 600; }
2619
+ .skip:focus { top: var(--space-3); text-decoration: none; }
2620
+ .visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
2621
+
2622
+ /* Shell */
2623
+ .brand { display: inline-flex; align-items: center; gap: 9px; min-width: 0; color: var(--ink); font-weight: 600; font-size: var(--text-lg); letter-spacing: -0.015em; }
2555
2624
  .brand:hover { text-decoration: none; }
2556
- .app { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; background: linear-gradient(to right, var(--side) 239px, var(--line) 239px, var(--line) 240px, var(--paper) 240px); }
2557
- .side { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; gap: 16px; padding: 16px 12px; }
2558
- .side-top { display: grid; gap: 14px; padding: 0 6px; }
2559
- .workspace { display: flex; align-items: center; gap: 8px; margin: 0; padding: 6px; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); min-width: 0; }
2560
- .workspace-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2561
- .workspace select { flex: 1; min-width: 0; height: 28px; border: 0; padding: 0 2px; font-weight: 600; background: transparent; }
2562
- .avatar { display: inline-grid; place-items: center; width: 24px; height: 24px; flex: none; border-radius: 6px; background: var(--accent-wash); color: var(--accent); font-size: 12px; font-weight: 700; }
2563
- .nav { display: grid; gap: 2px; }
2564
- .nav a { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: 7px; color: var(--ink-2); font-weight: 500; }
2625
+ .brand .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2626
+ .app { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
2627
+ .side { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; gap: var(--space-5); padding: var(--space-4) var(--space-3); background: var(--surface-2); border-right: 1px solid var(--line); min-width: 0; }
2628
+ .side-top { display: grid; gap: var(--space-4); padding: var(--space-1) var(--space-2) 0; min-width: 0; }
2629
+ .workspace { display: flex; align-items: center; gap: var(--space-2); margin: 0; padding: 5px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); min-width: 0; }
2630
+ .workspace-name { font-weight: 600; font-size: var(--text-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2631
+ .workspace select { flex: 1; min-width: 0; height: 26px; border: 0; padding: 0 18px 0 2px; font-weight: 600; font-size: var(--text-md); background-color: transparent; box-shadow: none; text-overflow: ellipsis; }
2632
+ .workspace select:focus { box-shadow: none; }
2633
+ .workspace:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
2634
+ .avatar { display: inline-grid; place-items: center; width: 24px; height: 24px; flex: none; border-radius: var(--radius-sm); background: var(--ink); color: var(--surface); font-size: 11px; font-weight: 700; }
2635
+ .nav { display: grid; gap: 1px; }
2636
+ .nav a { position: relative; display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 10px; border-radius: var(--radius); color: var(--ink-2); font-weight: 500; font-size: var(--text-md); transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease); }
2637
+ .nav a .icon { color: var(--ink-3); transition: color var(--fast) var(--ease); }
2565
2638
  .nav a:hover { background: var(--hover); color: var(--ink); text-decoration: none; }
2566
- .nav a[aria-current="page"] { background: var(--hover); color: var(--ink); font-weight: 600; }
2567
- .nav a[aria-current="page"] .icon { color: var(--accent); }
2568
- .me { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 10px 0; border-top: 1px solid var(--line); color: var(--ink-3); font-size: 13px; }
2569
- .me form { margin: 0; }
2570
- .login { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2571
- .page { width: 100%; max-width: 1120px; padding: 28px 40px 64px; display: grid; gap: 20px; align-content: start; }
2572
- .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
2573
- h1 { font-size: 22px; line-height: 1.25; margin: 0; letter-spacing: -0.015em; font-weight: 650; }
2574
- h2 { font-size: 14px; margin: 0 0 12px; font-weight: 600; }
2575
- h3 { font-size: 12px; margin: 0; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em; }
2639
+ .nav a:hover .icon { color: var(--ink-2); }
2640
+ .nav a[aria-current="page"] { background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); }
2641
+ .nav a[aria-current="page"] .icon { color: var(--ink); }
2642
+ .me { margin-top: auto; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-2) 0; border-top: 1px solid var(--line); color: var(--ink-2); font-size: var(--text-md); min-width: 0; }
2643
+ .me-avatar { width: 22px; height: 22px; border-radius: var(--radius-full); background: var(--sunken); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line); }
2644
+ .me form { margin: 0 0 0 auto; }
2645
+ .login { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
2646
+ .sign-out .icon { width: 15px; height: 15px; }
2647
+ .page { width: 100%; max-width: 1600px; margin-inline: auto; padding: var(--space-8) var(--space-8) 72px; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-5); align-content: start; min-width: 0; }
2648
+ .page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; min-height: 36px; margin-bottom: var(--space-1); }
2649
+
2650
+ /* Type */
2651
+ h1 { font-size: var(--text-xl); line-height: 1.25; margin: 0; letter-spacing: -0.022em; font-weight: 600; overflow-wrap: anywhere; }
2652
+ h2 { font-size: var(--text-lg); line-height: 1.35; margin: 0 0 var(--space-4); font-weight: 600; letter-spacing: -0.01em; }
2653
+ h3 { font: 500 var(--text-xs)/1.4 var(--mono); margin: 0; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
2576
2654
  p { margin: 0 0 10px; color: var(--ink-2); }
2577
- .fine { font-size: 12.5px; color: var(--ink-3); margin: 8px 0 0; }
2655
+ .fine { font-size: var(--text-sm); color: var(--ink-3); margin: var(--space-3) 0 0; }
2578
2656
  .muted { color: var(--ink-3); }
2579
- .panel, .card { border: 1px solid var(--line); border-radius: 12px; padding: 18px 20px; background: var(--paper); box-shadow: var(--shadow); min-width: 0; }
2580
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
2581
- .stat { display: grid; gap: 4px; padding: 14px 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); box-shadow: var(--shadow); color: var(--ink); }
2582
- .stat:hover { border-color: var(--ink-3); text-decoration: none; }
2583
- .stat-label { font-size: 12.5px; color: var(--ink-3); }
2584
- .stat-value { font-size: 24px; font-weight: 650; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
2585
- .split { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 20px; align-items: start; }
2586
- .setup .steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
2587
- .steps li { display: flex; align-items: center; gap: 10px; color: var(--ink-2); }
2588
- .steps li.done { color: var(--ink-3); text-decoration: line-through; }
2589
- .check { display: inline-grid; place-items: center; width: 20px; height: 20px; border-radius: 999px; border: 1.5px solid var(--line); color: var(--accent-ink); }
2590
- .done .check { background: var(--good); border-color: var(--good); }
2657
+ strong { color: var(--ink); font-weight: 600; }
2658
+
2659
+ /* Surfaces */
2660
+ .panel, .card { border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--space-5) var(--space-6); background: var(--surface); min-width: 0; }
2661
+ .stack { display: grid; gap: var(--space-5); align-content: start; min-width: 0; }
2662
+ .split { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: var(--space-5); align-items: start; }
2663
+
2664
+ /* Stats */
2665
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); overflow: hidden; }
2666
+ .stat { position: relative; display: grid; gap: 6px; padding: var(--space-4) var(--space-5) 18px; color: var(--ink); box-shadow: -1px 0 0 var(--line); transition: background-color var(--fast) var(--ease); }
2667
+ a.stat:hover { background: var(--surface-2); text-decoration: none; }
2668
+ a.stat::after { content: ""; position: absolute; top: 18px; right: 18px; width: 6px; height: 6px; border-top: 1.5px solid var(--ink-3); border-right: 1.5px solid var(--ink-3); transform: rotate(45deg); opacity: 0; transition: opacity var(--fast) var(--ease); }
2669
+ a.stat:hover::after, a.stat:focus-visible::after { opacity: 1; }
2670
+ a.stat:focus-visible { outline-offset: -2px; border-radius: 0; }
2671
+ .stat-label { font: 500 var(--text-xs)/1.4 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
2672
+ .stat-value { font-size: var(--text-2xl); line-height: 1.1; font-weight: 500; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
2673
+
2674
+ /* Setup checklist */
2675
+ .setup .steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; counter-reset: step; }
2676
+ .steps li { display: flex; align-items: center; gap: var(--space-3); min-height: 36px; color: var(--ink); font-weight: 500; }
2677
+ .steps li.done { color: var(--ink-3); font-weight: 400; text-decoration: line-through; text-decoration-color: var(--line-strong); }
2678
+ .check { display: inline-grid; place-items: center; width: 20px; height: 20px; flex: none; border-radius: var(--radius-full); border: 1.5px dashed var(--line-strong); color: var(--surface); }
2679
+ .done .check { background: var(--good); border: 0; }
2591
2680
  .check .icon { width: 12px; height: 12px; stroke-width: 3; }
2592
- .day { margin: 14px 0 6px; }
2681
+
2682
+ /* Agenda and activity */
2683
+ .day { margin: var(--space-5) 0 var(--space-1); }
2593
2684
  .day:first-of-type { margin-top: 0; }
2594
2685
  .slots, .feed { list-style: none; margin: 0; padding: 0; }
2595
- .slot { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: 12px; align-items: center; padding: 9px 0; border-bottom: 1px solid var(--line-2); }
2686
+ .slot { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: var(--space-3); align-items: center; padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
2596
2687
  .slot:last-child { border-bottom: 0; }
2597
- .when { display: grid; font-weight: 600; font-variant-numeric: tabular-nums; }
2598
- .until { font-weight: 400; font-size: 12px; color: var(--ink-3); }
2688
+ .when { display: grid; font-weight: 600; font-size: var(--text-md); font-variant-numeric: tabular-nums; }
2689
+ .until { font-weight: 400; font-size: var(--text-sm); color: var(--ink-3); }
2599
2690
  .what { display: grid; min-width: 0; }
2600
- .title { font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2691
+ .title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2601
2692
  .on { color: var(--ink-3); font-weight: 400; }
2602
- .meta { font-size: 12.5px; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2603
- .event { display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 12px; align-items: start; padding: 8px 0; }
2604
- .dot { width: 8px; height: 8px; margin-top: 6px; border-radius: 999px; background: var(--ink-3); }
2605
- .dot.good { background: var(--good); }
2606
- .dot.bad { background: var(--bad); }
2607
- .pill { display: inline-block; font-size: 11.5px; font-weight: 600; line-height: 18px; padding: 1px 8px; border-radius: 999px; background: var(--wash); color: var(--ink-2); white-space: nowrap; }
2608
- .pill.booking, .pill.active { background: var(--good-wash); color: var(--good); }
2609
- .pill.event { background: var(--accent-wash); color: var(--accent); }
2610
- .pill.held { background: var(--held-wash); color: var(--held); }
2611
- .pill.revoked, .pill.expired { color: var(--ink-3); }
2612
- .endpoints { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 16px; margin: 12px 0 0; align-items: center; }
2613
- .endpoints dt { color: var(--ink-3); font-weight: 600; font-size: 12.5px; }
2614
- .endpoints dd { margin: 0; display: flex; align-items: center; gap: 10px; min-width: 0; }
2615
- .endpoints code { overflow-wrap: anywhere; }
2616
- .facts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin: 0; }
2693
+ .meta { font-size: var(--text-sm); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2694
+ .feed { position: relative; }
2695
+ .event { position: relative; display: grid; grid-template-columns: 12px minmax(0, 1fr); gap: var(--space-3); align-items: start; padding: 7px 0; }
2696
+ .event:not(:last-child)::before { content: ""; position: absolute; left: 5.5px; top: 22px; bottom: -8px; width: 1px; background: var(--line); }
2697
+ .dot { position: relative; width: 12px; height: 12px; margin-top: 4px; border-radius: var(--radius-full); background: var(--surface); box-shadow: inset 0 0 0 3px var(--ink-3); }
2698
+ .dot.good { box-shadow: inset 0 0 0 3px var(--good); }
2699
+ .dot.bad { box-shadow: inset 0 0 0 3px var(--bad); }
2700
+
2701
+ /* Pills */
2702
+ .pill { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); font-weight: 600; line-height: 18px; padding: 1px 8px 1px 7px; border-radius: var(--radius-full); background: var(--sunken); color: var(--ink-2); white-space: nowrap; box-shadow: inset 0 0 0 1px var(--line); }
2703
+ .pill::before { content: ""; width: 6px; height: 6px; border-radius: var(--radius-full); background: currentColor; opacity: 0.9; }
2704
+ .pill.booking, .pill.active { background: var(--good-wash); color: var(--good); box-shadow: none; }
2705
+ .pill.event { background: var(--accent-wash); color: var(--accent); box-shadow: none; }
2706
+ .pill.held { background: var(--warn-wash); color: var(--warn); box-shadow: none; }
2707
+ .pill.revoked, .pill.expired { background: transparent; color: var(--ink-3); }
2708
+
2709
+ /* Definition lists */
2710
+ .endpoints { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-2) var(--space-4); margin: var(--space-4) 0 0; align-items: center; }
2711
+ .endpoints dt { font: 500 var(--text-xs)/1 var(--mono); color: var(--ink-3); letter-spacing: 0.06em; }
2712
+ .endpoints dd { margin: 0; display: flex; align-items: center; gap: var(--space-2); min-width: 0; padding: 4px 4px 4px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); }
2713
+ .endpoints code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2714
+ .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-2) var(--space-6); margin: 0; }
2617
2715
  .facts dt { color: var(--ink-3); }
2618
- .facts dd { margin: 0; }
2619
- .create { display: flex; flex-wrap: wrap; align-items: end; gap: 12px; margin: 0; }
2620
- label { display: grid; gap: 5px; font-size: 12.5px; font-weight: 500; color: var(--ink-2); }
2621
- input, select { font: inherit; height: 34px; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); color: var(--ink); min-width: 0; }
2622
- input:focus, select:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-wash); }
2623
- button, .button { font: inherit; font-weight: 600; height: 34px; padding: 0 14px; border-radius: 8px; border: 1px solid var(--line); background: var(--paper); color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-shadow: var(--shadow); }
2624
- button:hover, .button:hover { background: var(--hover); text-decoration: none; }
2625
- .primary, .primary:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
2626
- .primary:hover { filter: brightness(1.06); }
2627
- .wide { width: 100%; height: 40px; }
2628
- button.danger { border-color: var(--bad); color: var(--bad); }
2629
- button.quiet { height: auto; padding: 2px 6px; border: 0; background: none; box-shadow: none; color: var(--ink-3); font-weight: 500; font-size: 12.5px; }
2716
+ .facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
2717
+ .page > .facts { padding: var(--space-4) var(--space-6); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); }
2718
+
2719
+ /* Forms */
2720
+ label { display: grid; gap: 6px; font-size: var(--text-sm); font-weight: 500; color: var(--ink-2); min-width: 0; }
2721
+ input, select, textarea { font: inherit; font-size: 14px; color: var(--ink); background-color: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius); min-width: 0; transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease); }
2722
+ input, select { height: 36px; padding: 0 var(--space-3); }
2723
+ input::placeholder, textarea::placeholder { color: var(--ink-3); opacity: 0.8; }
2724
+ input:hover, select:hover, textarea:hover { border-color: var(--ink-3); }
2725
+ input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--ring); }
2726
+ input[readonly] { background-color: var(--surface-2); }
2727
+ select { -webkit-appearance: none; appearance: none; padding-right: 32px; cursor: pointer; background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%), linear-gradient(135deg, var(--ink-3) 50%, transparent 50%); background-position: calc(100% - 16px) 50%, calc(100% - 11px) 50%; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; }
2728
+ input[type="checkbox"], input[type="radio"] { width: 16px; height: 16px; margin: 0; padding: 0; accent-color: var(--ink); flex: none; }
2729
+ input[type="time"] { font-variant-numeric: tabular-nums; }
2730
+ textarea { padding: 10px var(--space-3); resize: vertical; min-height: 72px; line-height: 1.5; }
2731
+ .create { display: flex; flex-wrap: wrap; align-items: end; gap: var(--space-3); margin: 0; }
2732
+ .create .grow { flex: 1 1 220px; }
2733
+ label > input, label > select, label > .unit-input { width: 100%; }
2734
+ label.choice > input { width: 16px; }
2735
+ .create label:not(.grow) { flex: 0 1 auto; }
2736
+ form > label { margin-bottom: var(--space-3); }
2737
+ .grid-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-4) var(--space-3); margin-bottom: var(--space-4); align-items: end; }
2738
+ .unit-input { display: flex; align-items: center; gap: var(--space-2); }
2739
+ .unit-input input { width: 100%; }
2740
+ .unit-input span { color: var(--ink-3); white-space: nowrap; }
2741
+ .choice { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 400; color: var(--ink); cursor: pointer; }
2742
+ form .choice { margin: var(--space-1) 0 var(--space-3); }
2743
+ .modes { border: 0; padding: 0; margin: 0 0 var(--space-5); display: grid; gap: var(--space-2); }
2744
+ .modes legend { font-weight: 600; color: var(--ink); padding: 0; margin-bottom: var(--space-2); }
2745
+ .modes .choice { padding: 10px var(--space-3); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); margin: 0; }
2746
+ .modes .choice:has(input:checked) { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
2747
+ .types { margin: var(--space-1) 0 var(--space-4); }
2748
+ .types summary { cursor: pointer; color: var(--ink-2); font-size: var(--text-md); width: fit-content; }
2749
+ .checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--space-2) var(--space-4); margin-top: var(--space-3); padding: var(--space-4); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); }
2750
+ .checks .choice { font-size: var(--text-md); margin: 0; }
2751
+
2752
+ /* Buttons */
2753
+ button, .button { font: inherit; font-size: var(--text-md); font-weight: 500; height: 36px; padding: 0 var(--space-4); border-radius: var(--radius); border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); white-space: nowrap; transition: background-color var(--fast) var(--ease), border-color var(--fast) var(--ease), color var(--fast) var(--ease), transform var(--fast) var(--ease); }
2754
+ button:hover, .button:hover { background: var(--hover); border-color: var(--ink-3); text-decoration: none; }
2755
+ button:active, .button:active { transform: translateY(0.5px); }
2756
+ .primary, a.primary { background: var(--button); border-color: var(--button); color: var(--button-ink); }
2757
+ .primary:hover, a.primary:hover { background: var(--button-hover); border-color: var(--button-hover); color: var(--button-ink); }
2758
+ .wide { width: 100%; height: 42px; }
2759
+ button.danger { background: var(--bad); border-color: var(--bad); color: var(--bad-ink); }
2760
+ button.danger:hover { background: var(--bad); border-color: var(--bad); filter: brightness(1.08); }
2761
+ button.quiet { height: 28px; padding: 0 var(--space-2); border: 0; background: none; color: var(--ink-3); font-weight: 500; font-size: var(--text-sm); }
2630
2762
  button.quiet:hover { color: var(--ink); background: var(--hover); }
2631
- :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2632
- .table { overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); box-shadow: var(--shadow); }
2633
- table { width: 100%; border-collapse: collapse; }
2634
- th, td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--line-2); vertical-align: middle; }
2635
- th { font-weight: 600; color: var(--ink-3); font-size: 12px; background: var(--wash); border-bottom-color: var(--line); white-space: nowrap; }
2636
- td { white-space: nowrap; }
2637
- td.strong { font-weight: 600; white-space: normal; min-width: 120px; }
2763
+ .row-actions, .action-buttons { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; margin-top: var(--space-4); }
2764
+ form.inline { display: inline; margin: 0; }
2765
+
2766
+ /* Tables */
2767
+ .table { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); }
2768
+ table { width: 100%; border-collapse: collapse; font-size: var(--text-md); }
2769
+ th, td { text-align: left; padding: 11px 10px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
2770
+ th:first-child, td:first-child { padding-left: var(--space-4); }
2771
+ th:last-child, td:last-child { padding-right: var(--space-4); }
2772
+ th { font: 500 var(--text-xs)/1.4 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; background: var(--surface-2); border-bottom-color: var(--line); white-space: nowrap; padding-top: 9px; padding-bottom: 9px; }
2773
+ td { white-space: nowrap; color: var(--ink-2); font-variant-numeric: tabular-nums; }
2774
+ tbody tr { transition: background-color var(--fast) var(--ease); }
2775
+ tbody tr:hover { background: var(--surface-2); }
2776
+ td.strong { color: var(--ink); font-weight: 500; white-space: normal; min-width: 140px; }
2638
2777
  tr:last-child td { border-bottom: 0; }
2639
- .actions { white-space: nowrap; text-align: right; }
2640
- .act { display: inline-block; position: relative; margin-left: 8px; text-align: left; }
2641
- .act summary { cursor: pointer; color: var(--ink-2); font-weight: 500; list-style: none; }
2778
+ td.wrap { white-space: normal; min-width: 160px; }
2779
+ .truncate { display: block; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2780
+ .truncate.url { max-width: 340px; }
2781
+ .truncate.id { max-width: 150px; }
2782
+ .chip { display: inline-block; padding: 1px 6px; border-radius: var(--radius-sm); background: var(--sunken); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line); font-size: 12px; }
2783
+ .cell { display: grid; justify-items: start; gap: 2px; min-width: 0; }
2784
+ .cell > * { max-width: 100%; }
2785
+ td .meta { display: block; margin-top: 2px; }
2786
+ .meta-link { font-size: var(--text-sm); }
2787
+ .wrap, td.wrap .meta, .meta.wrap { white-space: normal; overflow-wrap: anywhere; }
2788
+ .actions { white-space: nowrap; text-align: right; width: 1%; }
2789
+ .actions button { height: 28px; padding: 0 10px; font-size: var(--text-sm); }
2790
+ td .meta.wrap { min-width: 200px; max-width: 340px; }
2791
+ .week input { width: 100%; }
2792
+ .week th[scope="row"] { font: 500 var(--text-md)/1.4 var(--font); text-transform: none; letter-spacing: 0; color: var(--ink); background: none; }
2793
+ form > .table { margin-bottom: var(--space-3); }
2794
+
2795
+ /* Confirmations */
2796
+ .act { display: inline-block; position: relative; margin-left: var(--space-1); text-align: left; }
2797
+ .act summary { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: var(--radius-sm); cursor: pointer; color: var(--ink-2); font-weight: 500; font-size: var(--text-sm); list-style: none; transition: background-color var(--fast) var(--ease), color var(--fast) var(--ease); }
2642
2798
  .act summary::-webkit-details-marker { display: none; }
2799
+ .act summary:hover { background: var(--hover); color: var(--ink); }
2800
+ .act[open] summary { background: var(--hover); color: var(--ink); }
2643
2801
  .act.danger summary { color: var(--bad); }
2644
- .pop { position: absolute; right: 0; z-index: 2; width: 260px; margin-top: 8px; padding: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper); box-shadow: 0 8px 24px rgba(15, 17, 21, 0.12); white-space: normal; }
2645
- .pop p { font-size: 12.5px; }
2646
- .notice { border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px; background: var(--wash); color: var(--ink); margin: 0; }
2647
- .notice.warn { background: var(--warn-wash); color: var(--warn); border-color: transparent; }
2802
+ .act.danger summary:hover, .act.danger[open] summary { background: var(--bad-wash); color: var(--bad); }
2803
+ .pop { position: absolute; right: 0; z-index: 5; width: 280px; margin-top: 6px; padding: var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--pop-shadow); white-space: normal; animation: pop-in var(--slow) var(--ease); transform-origin: top right; }
2804
+ .pop p { font-size: var(--text-md); color: var(--ink); margin-bottom: var(--space-3); }
2805
+ .pop button { width: 100%; }
2806
+ .row-actions .pop { left: 0; right: auto; transform-origin: top left; }
2807
+ /* In a table the confirmation opens in place, growing its row. .table scrolls (overflow-x: auto,
2808
+ which makes it clip both ways), so a floating .pop past its last row was cut off: Revoke on a
2809
+ one-key account opened nothing anyone could click. */
2810
+ .table .pop { position: static; width: 280px; max-width: 100%; margin: var(--space-2) 0 var(--space-1) auto; box-shadow: none; text-align: left; }
2811
+ @keyframes pop-in { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: none; } }
2812
+
2813
+ /* Notices and empty states */
2814
+ .notice { display: block; border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-3) var(--space-4); background: var(--surface); color: var(--ink); margin: 0; font-size: var(--text-md); }
2815
+ .notice[role="alert"] { border-color: var(--bad-line); background: var(--bad-wash); color: var(--ink); box-shadow: inset 3px 0 0 var(--bad); }
2816
+ .notice.warn, .notice.warn[role="alert"] { border-color: var(--warn-line); background: var(--warn-wash); color: var(--warn); box-shadow: inset 3px 0 0 var(--warn); }
2817
+ .notice p { color: inherit; margin: 0 0 var(--space-2); }
2648
2818
  .empty { color: var(--ink-3); margin: 0; }
2649
- .once { max-width: 720px; }
2650
- .copy { display: flex; gap: 8px; margin: 12px 0 20px; }
2819
+ .empty-state { display: grid; justify-items: center; gap: var(--space-3); padding: var(--space-12) var(--space-6); border: 1px dashed var(--line-strong); border-radius: var(--radius-lg); text-align: center; }
2820
+ .empty-state .empty { max-width: 380px; color: var(--ink-2); }
2821
+ .empty-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--radius); background: var(--surface); color: var(--ink-2); box-shadow: inset 0 0 0 1px var(--line), 0 1px 2px rgba(0, 0, 0, 0.04); }
2822
+ .empty-icon .icon { width: 20px; height: 20px; }
2823
+
2824
+ /* Shown once */
2825
+ .once { max-width: 720px; padding: var(--space-8); display: grid; gap: var(--space-2); }
2826
+ .once h2 { margin: var(--space-4) 0 0; font-size: var(--text-md); }
2827
+ .once > .button { justify-self: start; margin-top: var(--space-3); }
2828
+ .copy { display: flex; gap: var(--space-2); margin: var(--space-2) 0 var(--space-3); }
2651
2829
  .copy input { flex: 1; }
2652
- pre { background: var(--wash); border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
2830
+ pre { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-3) var(--space-4); margin: 0; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); line-height: 1.6; }
2653
2831
  .hours { margin: 0; padding-left: 18px; color: var(--ink-2); }
2654
- .back::before { content: "← "; }
2655
- .modes { border: 0; padding: 0; margin: 0 0 16px; display: grid; gap: 8px; }
2656
- .modes legend { font-weight: 600; color: var(--ink); padding: 0; margin-bottom: 4px; }
2657
- .choice { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--ink); }
2658
- .choice input { height: auto; }
2659
- .week input { width: 100%; }
2660
- .week th[scope="row"] { font-size: 13px; color: var(--ink); background: none; }
2661
- form > .table { margin-bottom: 12px; }
2662
- .bare { background: var(--wash); }
2663
- .bare-bar { padding: 20px 24px; }
2664
- .bare-main { display: grid; place-items: start center; padding: 8vh 16px 64px; }
2665
- .sign-in { width: 100%; max-width: 400px; padding: 28px; border: 1px solid var(--line); border-radius: 14px; background: var(--paper); box-shadow: var(--shadow); display: grid; gap: 12px; }
2832
+ .back { display: inline-flex; align-items: center; gap: 6px; color: var(--ink-3); font-size: var(--text-md); font-weight: 500; }
2833
+ .back::before { content: ""; width: 6px; height: 6px; border-left: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg); }
2834
+ .back:hover { color: var(--ink); text-decoration: none; }
2835
+ .page > p:has(> .back) { margin: 0 0 calc(-1 * var(--space-3)); }
2836
+ .page > section:not([class]) { display: grid; gap: var(--space-3); }
2837
+ .page > section:not([class]) > h1 { margin-bottom: var(--space-1); }
2838
+
2839
+ /* Signed out */
2840
+ .bare { min-height: 100vh; }
2841
+ .bare-bar { padding: var(--space-5) var(--space-6); }
2842
+ .bare-main { display: grid; place-items: start center; padding: 10vh var(--space-4) var(--space-12); }
2843
+ .sign-in { width: 100%; max-width: 400px; padding: var(--space-8); border: 1px solid var(--line); border-radius: 16px; background: var(--surface); display: grid; gap: var(--space-3); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 24px 48px -24px rgba(0, 0, 0, 0.12); }
2844
+ .sign-in-mark { display: grid; place-items: center; width: 44px; height: 44px; margin-bottom: var(--space-2); border-radius: var(--radius-lg); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line); color: var(--ink); }
2845
+ .sign-in-mark .mark { width: 24px; height: 22px; }
2666
2846
  .sign-in h1 { font-size: 20px; }
2667
2847
  .sign-in p { margin: 0; }
2668
- .stack { display: grid; gap: 20px; align-content: start; min-width: 0; }
2669
- .grid-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 12px; align-items: end; }
2670
- .cell { display: grid; justify-items: start; gap: 2px; }
2671
- .unit-input { display: flex; align-items: center; gap: 8px; }
2672
- .unit-input input { width: 100%; }
2673
- .unit-input span { color: var(--ink-3); white-space: nowrap; }
2674
- form .choice { margin: 4px 0 10px; }
2675
- .types { margin: 12px 0; }
2676
- .types summary { cursor: pointer; color: var(--ink-2); font-size: 13px; }
2677
- .checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px 16px; margin-top: 10px; }
2678
- .checks .choice { font-size: 13px; margin: 0; }
2679
- td .meta { display: block; margin-top: 2px; }
2680
- td[data-label] > .pill { justify-self: start; }
2681
- td.wrap, .wrap { white-space: normal; overflow-wrap: anywhere; }
2682
- form.inline { display: inline; margin: 0; }
2683
- form > label { margin-bottom: 4px; }
2684
- .chat { display: grid; gap: 16px; }
2685
- .thread { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
2686
- .msg { max-width: 78%; padding: 10px 14px; border-radius: 12px; background: var(--wash); border: 1px solid var(--line); }
2848
+ .sign-in .button { margin-top: var(--space-3); }
2849
+ .bare-main > .notice { max-width: 480px; }
2850
+
2851
+ /* Assistant */
2852
+ .chat { display: grid; gap: var(--space-5); }
2853
+ .thread { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
2854
+ .msg { max-width: 78%; padding: 10px 14px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--line); }
2687
2855
  .msg p { margin: 0; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
2688
- .msg.mine { justify-self: end; background: var(--accent-wash); border-color: transparent; }
2689
- .msg.bot { justify-self: start; }
2690
- .tool-line { font-size: 12.5px; color: var(--ink-3); padding-left: 4px; }
2691
- .action-card { justify-self: start; width: min(100%, 520px); padding: 14px 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); box-shadow: var(--shadow); display: grid; gap: 10px; }
2692
- .action-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
2693
- .action-card .facts { font-size: 13px; }
2694
- .action-card .facts dd { overflow-wrap: anywhere; }
2695
- .action-card pre.wrap { white-space: pre-wrap; margin: 0; padding: 8px; font-size: 12px; }
2856
+ .msg.mine { justify-self: end; background: var(--button); border-color: var(--button); border-bottom-right-radius: 4px; }
2857
+ .msg.mine p { color: var(--button-ink); }
2858
+ .msg.bot { justify-self: start; border-bottom-left-radius: 4px; }
2859
+ .tool-line { font: 400 var(--text-sm)/1.5 var(--mono); color: var(--ink-3); padding-left: var(--space-1); }
2860
+ .thread > .notice { justify-self: start; }
2861
+ .action-card { justify-self: start; width: min(100%, 540px); padding: var(--space-4) var(--space-5); border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--pop-shadow); display: grid; gap: var(--space-3); }
2862
+ .action-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
2863
+ .action-card .facts { font-size: var(--text-md); gap: 6px var(--space-4); }
2864
+ .action-card .facts dt::first-letter { text-transform: uppercase; }
2865
+ .action-card pre.wrap { white-space: pre-wrap; margin: 0; padding: var(--space-2); font-size: 12px; }
2696
2866
  .action-summary { margin: 0; color: var(--ink); font-weight: 500; overflow-wrap: anywhere; }
2697
- .action-buttons, .row-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
2698
- .composer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: end; margin: 0; }
2699
- textarea { font: inherit; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); color: var(--ink); resize: vertical; min-height: 64px; min-width: 0; }
2700
- textarea:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-wash); }
2701
- .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
2702
- @media (max-width: 1000px) {
2867
+ .action-card .action-buttons { margin-top: var(--space-1); }
2868
+ .composer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3); align-items: end; margin: 0; padding-top: var(--space-4); border-top: 1px solid var(--line-soft); }
2869
+
2870
+ @media (max-width: 1080px) {
2703
2871
  .split { grid-template-columns: minmax(0, 1fr); }
2872
+ .page { padding: var(--space-6) var(--space-8) 64px; }
2704
2873
  }
2705
2874
  @media (max-width: 760px) {
2706
- .app { grid-template-columns: minmax(0, 1fr); background: var(--paper); }
2707
- .side { position: sticky; z-index: 3; height: auto; background: var(--side); border-bottom: 1px solid var(--line); flex-direction: row; flex-wrap: wrap; align-items: center; gap: 10px 12px; padding: 10px 16px 0; border-right: 0; }
2708
- .side-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0; flex: 1 1 100%; min-width: 0; }
2709
- .workspace { max-width: 60%; }
2710
- .nav { display: flex; gap: 2px; overflow-x: auto; flex: 1 1 auto; margin: 0 -4px; padding-bottom: 8px; scrollbar-width: none; }
2711
- .nav a { flex: none; padding: 6px 10px; }
2712
- .me { margin: 0 0 8px; padding: 0; border: 0; flex: none; }
2713
- .me .login { display: none; }
2714
- .page { padding: 20px 16px 48px; gap: 16px; }
2715
- .panel, .card { padding: 16px; }
2875
+ .app { grid-template-columns: minmax(0, 1fr); }
2876
+ .side { position: sticky; z-index: 4; top: 0; height: auto; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 var(--space-2); padding: 10px var(--space-4) 0; border-right: 0; border-bottom: 1px solid var(--line); background: var(--surface-2); }
2877
+ .side-top { grid-column: 1; display: flex; align-items: center; gap: var(--space-3); padding: 0; min-width: 0; }
2878
+ .side-top .brand .name { display: none; }
2879
+ .workspace { flex: 1; min-width: 0; padding: 3px 4px; }
2880
+ .workspace .avatar { display: none; }
2881
+ .me { grid-column: 2; grid-row: 1; margin: 0; padding: 0; border: 0; }
2882
+ .me-avatar, .me .login { display: none; }
2883
+ .sign-out { width: 36px; height: 36px; padding: 0; }
2884
+ .sign-out-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
2885
+ .nav { grid-column: 1 / -1; display: flex; gap: var(--space-1); overflow-x: auto; margin: var(--space-2) calc(-1 * var(--space-4)) 0; padding: 0 var(--space-4); scrollbar-width: none; scroll-padding: 0 var(--space-4); }
2886
+ .nav::-webkit-scrollbar { display: none; }
2887
+ .nav a { flex: none; height: 40px; padding: 0 10px; border-radius: 0; box-shadow: none; background: none; }
2888
+ .nav a:hover { background: none; }
2889
+ .nav a[aria-current="page"] { background: none; box-shadow: inset 0 -2px 0 var(--ink); }
2890
+ .page { padding: var(--space-5) var(--space-4) var(--space-12); gap: var(--space-4); }
2891
+ h1 { font-size: 20px; }
2892
+ .panel, .card { padding: var(--space-4); }
2893
+ .once { padding: var(--space-5); }
2716
2894
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2717
- .stat-value { font-size: 20px; }
2895
+ .stat { padding: 14px var(--space-4); box-shadow: -1px 0 0 var(--line), 0 -1px 0 var(--line); }
2896
+ .stat-value { font-size: 22px; }
2897
+ .create > * { flex: 1 1 100%; }
2898
+ .create button { width: 100%; }
2899
+ .endpoints { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
2900
+ .endpoints dd { margin-bottom: var(--space-2); }
2901
+ .endpoints code { white-space: normal; overflow-wrap: anywhere; }
2902
+ .facts { grid-template-columns: minmax(0, 1fr); gap: 0; }
2903
+ .facts dd { margin-bottom: var(--space-2); }
2904
+ .table { border-radius: var(--radius-lg); }
2718
2905
  thead { display: none; }
2719
2906
  table, tbody, tr, td { display: block; }
2720
- tr { border-bottom: 1px solid var(--line); padding: 8px 0; }
2907
+ tr { border-bottom: 1px solid var(--line); padding: 10px 0; }
2908
+ tbody tr:hover { background: none; }
2721
2909
  tr:last-child { border-bottom: 0; }
2722
- td { border: 0; padding: 3px 14px; white-space: normal; }
2723
- td[data-label] { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 8px; }
2724
- td[data-label]::before { content: attr(data-label); font-size: 12px; color: var(--ink-3); }
2725
- .actions { text-align: left; }
2726
- .act { margin: 4px 12px 0 0; }
2727
- .pop { left: 0; right: auto; }
2910
+ td { border: 0; padding: 3px var(--space-4); white-space: normal; }
2911
+ td[data-label] { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: var(--space-2); align-items: baseline; }
2912
+ td[data-label]::before { content: attr(data-label); font: 500 var(--text-xs)/1.6 var(--mono); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; }
2913
+ td.strong, td.wrap { min-width: 0; }
2914
+ td .meta.wrap { min-width: 0; max-width: none; }
2915
+ .truncate, .truncate.url, .truncate.id { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
2916
+ .week th[scope="row"] { display: block; padding: var(--space-2) var(--space-4) 0; }
2917
+ .actions { text-align: left; width: auto; padding-top: var(--space-2); }
2918
+ .act { margin: 0 var(--space-2) 0 0; }
2919
+ .act:first-child, form.inline + .act { margin-left: 0; }
2920
+ .act summary { border: 1px solid var(--line-strong); height: 32px; }
2921
+ .act.danger summary { border-color: var(--bad-line); }
2922
+ .pop { left: 0; right: auto; width: min(300px, calc(100vw - 48px)); transform-origin: top left; }
2923
+ .table .pop { margin-left: 0; }
2728
2924
  .msg { max-width: 92%; }
2729
2925
  .composer { grid-template-columns: minmax(0, 1fr); }
2926
+ .empty-state { padding: var(--space-8) var(--space-4); }
2927
+ .bare-main { padding-top: 6vh; }
2928
+ .sign-in { padding: var(--space-6); }
2929
+ }
2930
+ @media (prefers-reduced-motion: reduce) {
2931
+ *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; scroll-behavior: auto !important; }
2932
+ }
2933
+ @media (forced-colors: active) {
2934
+ .pill, .chip, .nav a[aria-current="page"] { border: 1px solid CanvasText; }
2730
2935
  }
2731
2936
  `;
2732
2937
  const ASSET_VERSIONS = Object.freeze({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slotlock",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "description": "Resource-first, deterministic calendar and availability engine for AI agents.",
6
6
  "license": "Apache-2.0",
@@ -3,7 +3,7 @@
3
3
  "schemaVersion": "1.0",
4
4
  "name": "Slotlock",
5
5
  "package": "slotlock",
6
- "version": "0.6.0",
6
+ "version": "0.6.2",
7
7
  "description": "Resource-first, deterministic calendar and availability engine for AI agents.",
8
8
  "intervalSemantics": {
9
9
  "boundary": "[)",