@bkazemi/verily 0.9.2 → 0.9.4

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/README.md CHANGED
@@ -168,7 +168,7 @@ import { init } from '@bkazemi/verily';
168
168
  const client = init({ backendUrl: '/api/verily' });
169
169
  ```
170
170
 
171
- Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.2/dist/verily.js`. The script defines a global `Verily`.
171
+ Without one, serve `node_modules/@bkazemi/verily/dist/verily.js` yourself, or load it from a CDN such as `https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.4/dist/verily.js`. The script defines a global `Verily`.
172
172
 
173
173
  Then place the badge wherever the account appears:
174
174
 
@@ -370,7 +370,7 @@ A static site only needs the script and the badge markup; the backend runs somew
370
370
 
371
371
  ```html
372
372
  <script
373
- src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.2/dist/verily.js"
373
+ src="https://cdn.jsdelivr.net/npm/@bkazemi/verily@0.9.4/dist/verily.js"
374
374
  defer
375
375
  ></script>
376
376
  <verily-badge
@@ -115,6 +115,7 @@ renew) {
115
115
  const header = node('header');
116
116
  const stamp = node('footer');
117
117
  const content = node('div', 'Loading…', 'steps');
118
+ const pane = node('div', '', 'pane');
118
119
  heading.id = 'verily-connect-title';
119
120
  dialog.setAttribute('aria-labelledby', heading.id);
120
121
  close.type = 'button';
@@ -123,7 +124,8 @@ renew) {
123
124
  content.setAttribute('aria-live', 'polite');
124
125
  stamp.append(stampLink(), node('span', version));
125
126
  header.append(heading, close);
126
- dialog.append(header, content, stamp);
127
+ pane.append(header, content, stamp);
128
+ dialog.append(pane);
127
129
  root.append(dialog);
128
130
  document.body.append(host);
129
131
  openDialogs.set(opener, dialog);
@@ -1,7 +1,7 @@
1
1
  import { type Evidence, type ExternalAccount } from '../core/index.js';
2
2
  /** The logotype as a link to the project, as both dialogs carry it in their footer. */
3
3
  export declare function stampLink(): HTMLAnchorElement;
4
- export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n .icon { display: grid; place-items: center; padding: 0; }\n .icon svg { width: 16px; height: 16px; }\n .title { display: flex; align-items: center; gap: 6px; min-width: 0; }\n .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }\n .all:hover { background: #f2f5f1; }\n .all svg { width: 16px; height: 16px; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n /* An element given its own display would otherwise go on showing while hidden. */\n [hidden] { display: none !important; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n /* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n .summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }\n .summary a.signed:hover { background: #f2f5f1; }\n .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }\n .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }\n .caution svg { display: block; width: 10px; height: 10px; }\n .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }\n .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .account a, .account strong { font-weight: 650; font-size: 14px; }\n /* The account's own name carries that weight; a link inside a line of prose does not. */\n .summary a, .method a, .additional a { font: inherit; }\n .reference { margin-top: 2px; }\n /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n .fingerprint span { display: inline-block; margin-right: .5ch; }\n .method { margin-top: 8px; }\n /* In the corner the heading leaves empty, apart from everything that was checked. */\n .listing { position: absolute; top: 12px; right: 14px; padding: 1px 8px; border: 1px solid #e0e6df; border-radius: 999px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .listing.preferred { border-color: #245f43; color: #245f43; }\n /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n .account.retired { background: #f6f8f5; }\n .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }\n /* The name, then a dot and the account's creation date. The date wraps under a long name. */\n /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }\n .since { position: relative; white-space: nowrap; }\n /* The dot sits in the gap before the date. */\n .since::before { content: \"\u00B7\"; position: absolute; left: -7px; transform: translateX(-50%); }\n .account.retired .mark { opacity: .45; }\n .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }\n summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }\n summary::-webkit-details-marker { display: none; }\n .fold { display: flex; align-items: center; gap: 4px; width: auto; height: auto; padding: 2px 8px 2px 4px; margin-left: -4px; border: 0; background: none; font: 550 12px/1.6 system-ui, sans-serif; }\n .fold svg, summary svg { width: 12px; height: 12px; }\n .fold[aria-expanded=\"true\"] svg, details[open] > summary svg { transform: rotate(90deg); }\n @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }\n /* The fold and what the holder may do share a line, until a question needs the whole of it. */\n .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }\n .account .bar .row { margin: 0 0 0 auto; }\n .additional { margin-top: 2px; padding-left: 12px; }\n .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }\n dl { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid #e5e9e3; display: grid; grid-template-columns: auto 1fr; gap: 5px 16px; font-size: 11px; }\n dt { color: #6b786f; }\n dd { margin: 0; text-align: right; overflow-wrap: anywhere; }\n .explanation { margin-top: 16px; }\n .action { width: auto; height: auto; padding: 9px 14px; font: 600 13px/1.3 system-ui, sans-serif; color: #23312b; text-align: left; }\n .action.primary { border-color: #245f43; background: #245f43; color: #fff; }\n .action.primary:hover { background: #1d4f37; }\n .action:disabled { opacity: .6; cursor: progress; }\n .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }\n .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }\n .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }\n .account .action { padding: 5px 10px; font-size: 12px; }\n .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }\n .dots svg { width: 16px; height: 16px; }\n /* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n .menu { position: fixed; inset: auto; margin: 0; min-width: 170px; padding: 6px; border: 1px solid #dce2de; border-radius: 10px; background: #fff; color: #23312b; box-shadow: 0 12px 40px #10201930; }\n .menu:popover-open { display: grid; }\n .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }\n .menu .danger { color: #b3261e; }\n .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }\n footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }\n footer a { display: flex; color: inherit; }\n .logo { display: block; height: 13px; }\n \n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n\n";
4
+ export declare const styles = "\n * { box-sizing: border-box; }\n dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 0; overflow: hidden; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }\n /* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners. */\n dialog[open] { display: flex; flex-direction: column; }\n .pane { min-height: 0; padding: 24px; overflow-y: auto; }\n dialog::backdrop { background: #15271f66; }\n header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }\n button { width: 32px; height: 32px; border: 1px solid #dce2de; border-radius: 8px; background: #fff; color: #52645a; font: 20px system-ui, sans-serif; cursor: pointer; }\n button:hover { background: #f2f5f1; }\n .icon { display: grid; place-items: center; padding: 0; }\n .icon svg { width: 16px; height: 16px; }\n .title { display: flex; align-items: center; gap: 6px; min-width: 0; }\n .all { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #52645a; }\n .all:hover { background: #f2f5f1; }\n .all svg { width: 16px; height: 16px; }\n a { color: #245f43; text-underline-offset: 3px; overflow-wrap: anywhere; }\n a:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid #357ce5; outline-offset: 3px; }\n /* An element given its own display would otherwise go on showing while hidden. */\n [hidden] { display: none !important; }\n .summary { display: flex; align-items: center; gap: 8px; margin-top: 16px; }\n .mark { width: 36px; height: 36px; flex-shrink: 0; }\n .provider { width: 14px; height: 14px; }\n .state { font-size: 14px; font-weight: 650; line-height: 1.4; }\n .muted { color: #6b786f; font-size: 12px; }\n .account { position: relative; }\n .veiled > :not(.checking) { filter: blur(4px); opacity: .55; user-select: none; }\n .checking { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: #23312b; font-size: 13px; font-weight: 600; }\n .spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin .7s linear infinite; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }\n /* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n .summary .signed { display: inline-block; margin-left: 5px; padding: 0 6px; border: 1px solid #cfdcd3; border-radius: 999px; font-size: 10px; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: 1px; }\n .summary a.signed:hover { background: #f2f5f1; }\n .summary .signed.bad { border-color: #b3261e; color: #b3261e; font-weight: 650; cursor: help; }\n .caution { display: inline-block; margin-left: 3px; color: #a15c00; vertical-align: -1.5px; }\n .caution svg { display: block; width: 10px; height: 10px; }\n .account { padding: 14px 16px; border: 1px solid #e0e6df; border-radius: 10px; margin-top: 12px; }\n .account h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .account a, .account strong { font-weight: 650; font-size: 14px; }\n /* The account's own name carries that weight; a link inside a line of prose does not. */\n .summary a, .method a, .additional a { font: inherit; }\n .reference { margin-top: 2px; }\n /* A fingerprint in fours: it breaks between groups, and copies as one unbroken word. */\n .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n .fingerprint span { display: inline-block; margin-right: .5ch; }\n .method { margin-top: 8px; }\n /* In the corner the heading leaves empty, apart from everything that was checked. */\n .listing { position: absolute; top: 12px; right: 14px; padding: 1px 8px; border: 1px solid #e0e6df; border-radius: 999px; color: #6b786f; font-size: 11px; font-weight: 550; }\n .listing.preferred { border-color: #245f43; color: #245f43; }\n /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */\n .account.retired { background: #f6f8f5; }\n .account.retired .identity > a, .account.retired .identity > strong, .account.retired .named > a, .account.retired .named > strong { color: #6b786f; }\n /* The name, then a dot and the account's creation date. The date wraps under a long name. */\n /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */\n .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }\n .since { position: relative; white-space: nowrap; }\n /* The dot sits in the gap before the date. */\n .since::before { content: \"\u00B7\"; position: absolute; left: -7px; transform: translateX(-50%); }\n .account.retired .mark { opacity: .45; }\n .group { margin: 20px 0 0; color: #6b786f; font-size: 11px; font-weight: 550; }\n summary.group { display: flex; align-items: center; gap: 4px; width: fit-content; border-radius: 4px; list-style: none; cursor: pointer; }\n summary::-webkit-details-marker { display: none; }\n .fold { display: flex; align-items: center; gap: 4px; width: auto; height: auto; padding: 2px 8px 2px 4px; margin-left: -4px; border: 0; background: none; font: 550 12px/1.6 system-ui, sans-serif; }\n .fold svg, summary svg { width: 12px; height: 12px; }\n .fold[aria-expanded=\"true\"] svg, details[open] > summary svg { transform: rotate(90deg); }\n @media (prefers-reduced-motion: no-preference) { .fold svg, summary svg { transition: transform .15s; } }\n /* The fold and what the holder may do share a line, until a question needs the whole of it. */\n .bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 10px; }\n .account .bar .row { margin: 0 0 0 auto; }\n .additional { margin-top: 2px; padding-left: 12px; }\n .joiner { display: block; width: 20px; height: 20px; margin: 8px auto -4px; color: #90a096; }\n dl { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid #e5e9e3; display: grid; grid-template-columns: auto 1fr; gap: 5px 16px; font-size: 11px; }\n dt { color: #6b786f; }\n dd { margin: 0; text-align: right; overflow-wrap: anywhere; }\n .explanation { margin-top: 16px; }\n .action { width: auto; height: auto; padding: 9px 14px; font: 600 13px/1.3 system-ui, sans-serif; color: #23312b; text-align: left; }\n .action.primary { border-color: #245f43; background: #245f43; color: #fff; }\n .action.primary:hover { background: #1d4f37; }\n .action:disabled { opacity: .6; cursor: progress; }\n .action.danger:hover { border-color: #b3261e; background: #fdecea; color: #b3261e; }\n .row { display: flex; justify-content: end; gap: 8px; margin-top: 16px; }\n .account .row { align-items: center; justify-content: start; flex-wrap: wrap; margin-top: 12px; }\n .account .action { padding: 5px 10px; font-size: 12px; }\n .account .action.dots { display: grid; place-items: center; align-self: stretch; width: 30px; padding: 0; }\n .dots svg { width: 16px; height: 16px; }\n /* Placed under its button each time it opens, above the dialog and clipped by none of it. */\n .menu { position: fixed; inset: auto; margin: 0; min-width: 170px; padding: 6px; border: 1px solid #dce2de; border-radius: 10px; background: #fff; color: #23312b; box-shadow: 0 12px 40px #10201930; }\n .menu:popover-open { display: grid; }\n .account .menu .action { border: 0; border-radius: 6px; padding: 6px 10px; font-weight: 550; }\n .menu .danger { color: #b3261e; }\n .menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid #e5e9e3; }\n footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }\n footer a { display: flex; color: inherit; }\n .logo { display: block; height: 13px; }\n /* Four or more accounts on a wide screen: two equal cards to a row. */\n @media (min-width: 960px) {\n dialog.many { width: min(900px, calc(100vw - 32px)); }\n dialog.many .tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 12px; }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall as\n the taller card's part, so the verdicts and details of neighbouring cards line up. */\n dialog.many .tiles > .account { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-top: 0; }\n /* The dates sit at the foot of the third part, so they line up too. */\n dialog.many .tiles .rest { display: flex; flex-direction: column; }\n dialog.many .tiles .rest > dl { margin-top: auto; }\n dialog.many .tiles .rest > :has(+ dl) { margin-bottom: 16px; }\n /* The site's card is one tile wide, centred over the two columns. */\n dialog.many .local { width: calc(50% - 6px); margin-inline: auto; text-align: center; }\n dialog.many .local h3, dialog.many .local .named { justify-content: center; }\n dialog.many .local .additional { padding-left: 0; }\n }\n \n @media (prefers-reduced-motion: no-preference) {\n .mark.current path:last-child { animation: verily-draw .8s cubic-bezier(.65, 0, .35, 1) .1s backwards; }\n }\n @keyframes verily-draw { from { stroke-dasharray: 0 176; } to { stroke-dasharray: 108 176; } }\n\n";
5
5
  export declare function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string, className?: string): HTMLElementTagNameMap[K];
6
6
  /**
7
7
  * Points an anchor out of the dialog. Every link here opens in a new tab: the dialog is
@@ -13,7 +13,10 @@ export function stampLink() {
13
13
  const openDialogs = new WeakMap();
14
14
  export const styles = `
15
15
  * { box-sizing: border-box; }
16
- dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }
16
+ dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 0; overflow: hidden; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }
17
+ /* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners. */
18
+ dialog[open] { display: flex; flex-direction: column; }
19
+ .pane { min-height: 0; padding: 24px; overflow-y: auto; }
17
20
  dialog::backdrop { background: #15271f66; }
18
21
  header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
19
22
  h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
@@ -61,7 +64,7 @@ export const styles = `
61
64
  .listing.preferred { border-color: #245f43; color: #245f43; }
62
65
  /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */
63
66
  .account.retired { background: #f6f8f5; }
64
- .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }
67
+ .account.retired .identity > a, .account.retired .identity > strong, .account.retired .named > a, .account.retired .named > strong { color: #6b786f; }
65
68
  /* The name, then a dot and the account's creation date. The date wraps under a long name. */
66
69
  /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */
67
70
  .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }
@@ -104,6 +107,22 @@ export const styles = `
104
107
  footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }
105
108
  footer a { display: flex; color: inherit; }
106
109
  .logo { display: block; height: 13px; }
110
+ /* Four or more accounts on a wide screen: two equal cards to a row. */
111
+ @media (min-width: 960px) {
112
+ dialog.many { width: min(900px, calc(100vw - 32px)); }
113
+ dialog.many .tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 12px; }
114
+ /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall as
115
+ the taller card's part, so the verdicts and details of neighbouring cards line up. */
116
+ dialog.many .tiles > .account { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-top: 0; }
117
+ /* The dates sit at the foot of the third part, so they line up too. */
118
+ dialog.many .tiles .rest { display: flex; flex-direction: column; }
119
+ dialog.many .tiles .rest > dl { margin-top: auto; }
120
+ dialog.many .tiles .rest > :has(+ dl) { margin-bottom: 16px; }
121
+ /* The site's card is one tile wide, centred over the two columns. */
122
+ dialog.many .local { width: calc(50% - 6px); margin-inline: auto; text-align: center; }
123
+ dialog.many .local h3, dialog.many .local .named { justify-content: center; }
124
+ dialog.many .local .additional { padding-left: 0; }
125
+ }
107
126
  ${markStyles}
108
127
  `;
109
128
  export function node(tag, text = '', className = '') {
@@ -256,6 +275,8 @@ export function namedCard(heading, external, ...extra) {
256
275
  }
257
276
  function side(heading, name, reference, url, since, extra) {
258
277
  const card = node('section', '', 'account');
278
+ // The heading, name and reference together, so a grid can line them up as one part.
279
+ const identity = node('div', '', 'identity');
259
280
  const title = node('h3');
260
281
  const value = node(url ? 'a' : 'strong', name);
261
282
  title.append(...heading);
@@ -265,18 +286,18 @@ function side(heading, name, reference, url, since, extra) {
265
286
  // The name and the date share a line, and the date wraps under a long name.
266
287
  const named = node('div', '', 'named');
267
288
  named.append(value, node('span', since, 'muted since'));
268
- card.append(title, named);
289
+ identity.append(title, named);
269
290
  }
270
291
  else
271
- card.append(title, value);
292
+ identity.append(title, value);
272
293
  if (typeof reference === 'string')
273
- card.append(node('div', reference, 'muted reference'));
294
+ identity.append(node('div', reference, 'muted reference'));
274
295
  else if (reference) {
275
296
  const groups = node('div', '', 'muted reference fingerprint');
276
297
  groups.append(...reference.map((group) => node('span', group)));
277
- card.append(groups);
298
+ identity.append(groups);
278
299
  }
279
- card.append(...extra);
300
+ card.append(identity, ...extra);
280
301
  return card;
281
302
  }
282
303
  /** What the holder is told before an account is retired, since it cannot be undone from here. */
@@ -337,7 +358,7 @@ function actionsMenu(more, choices) {
337
358
  if (bottom > innerHeight - edge)
338
359
  menu.style.top = `${Math.max(edge, more.getBoundingClientRect().top - gap - height)}px`;
339
360
  // It does not follow its button, so it does not stay open once the button moves.
340
- menu.closest('dialog')?.addEventListener('scroll', shut, { once: true });
361
+ menu.closest('.pane')?.addEventListener('scroll', shut, { once: true });
341
362
  choices[0].focus();
342
363
  });
343
364
  menu.addEventListener('click', shut);
@@ -532,6 +553,21 @@ function cautionMark() {
532
553
  * Opens and shuts what a card folds away: how its account was shown, every method of it
533
554
  * together so that none is put ahead of another, and its dates.
534
555
  */
556
+ /**
557
+ * Scrolls what a reader just opened into view, once it has been laid out. It moves only as
558
+ * far as needed, and puts the top in view when the whole does not fit in the pane.
559
+ */
560
+ function reveal(element) {
561
+ requestAnimationFrame(() => {
562
+ const pane = element.closest('.pane');
563
+ const tall = pane !== null && element.offsetHeight > pane.clientHeight;
564
+ const still = matchMedia('(prefers-reduced-motion: reduce)').matches;
565
+ element.scrollIntoView({
566
+ block: tall ? 'start' : 'nearest',
567
+ behavior: still ? 'auto' : 'smooth',
568
+ });
569
+ });
570
+ }
535
571
  function foldToggle(folded, fold) {
536
572
  const toggle = node('button', '', 'fold');
537
573
  let shown = fold.open;
@@ -547,6 +583,8 @@ function foldToggle(folded, fold) {
547
583
  toggle.onclick = () => {
548
584
  set(!shown);
549
585
  fold.toggle(shown);
586
+ if (shown)
587
+ reveal(folded.at(-1));
550
588
  };
551
589
  return toggle;
552
590
  }
@@ -623,22 +661,25 @@ function externalCard(evidence, manage, later = () => { }, fold, warned = () =>
623
661
  if (fold)
624
662
  bar.append(foldToggle([...methods, dates], fold), ...row);
625
663
  const logo = providerMark(evidence.provider, evidence.attestations.external[0].method);
664
+ // Everything under the verdict in one part, the third of the card's rows in a grid.
665
+ const rest = node('div', '', 'rest');
666
+ rest.append(...(fold ? [bar, ...methods, dates] : [...methods, dates, ...row]));
626
667
  const card = namedCard(
627
668
  // The mark and the name, or just the name. A mark that falls back to writing the name
628
669
  // would print it twice here, since this heading writes it either way.
629
670
  logo ? [logo, document.createTextNode(provider)] : [document.createTextNode(provider)], evidence.external,
630
671
  // Status first, then how it was shown, then when. The proof explains the state
631
672
  // above it, so it cannot sit before that state has been given.
632
- summary, ...(fold ? [] : methods),
673
+ summary,
633
674
  // How the holder lists the account, in their words and apart from how it was shown:
634
- // nothing checked it, so it is never among the methods above it.
675
+ // nothing checked it, so it is never among the methods below it.
635
676
  ...(evidence.status === 'revoked' || retired
636
677
  ? []
637
678
  : evidence.mark === 'preferred'
638
679
  ? [node('div', 'Preferred', 'listing preferred')]
639
680
  : evidence.mark === 'unused'
640
681
  ? [node('div', 'No longer used', 'listing')]
641
- : []), ...(fold ? [bar, ...methods, dates] : [dates, ...row]));
682
+ : []), rest);
642
683
  const surface = retired ? 'account retired' : 'account';
643
684
  card.className = surface;
644
685
  // A signed record is not read, or acted on, before its signature has been checked: until
@@ -675,7 +716,9 @@ function addRow(manage) {
675
716
  * The subject once, then each account linked to it on its own card, in the order given.
676
717
  * Retired accounts sit in a group of their own at the foot, under what says they are,
677
718
  * shut until it is opened unless no other account is left to read. Every record given is
678
- * of the one subject, which the caller has checked.
719
+ * of the one subject, which the caller has checked. Returns whether the cards a reader
720
+ * sees on opening are four or more: the accounts in use, or the retired ones when
721
+ * nothing else is left and their group starts open.
679
722
  */
680
723
  function render(content, records, manage, later, folds = { open: false, cards: new Set() }) {
681
724
  const [first] = records;
@@ -689,6 +732,7 @@ function render(content, records, manage, later, folds = { open: false, cards: n
689
732
  site: first.siteName,
690
733
  provider: first.providerName,
691
734
  }));
735
+ localCard.className += ' local';
692
736
  const retired = records.filter((record) => record.status === 'retired');
693
737
  const listed = records.filter((record) => record.status !== 'retired');
694
738
  // A card alone in its group has room for its dates. Among others it folds them.
@@ -705,9 +749,11 @@ function render(content, records, manage, later, folds = { open: false, cards: n
705
749
  const count = node('summary', '', 'group');
706
750
  count.append(foldMark(), document.createTextNode(`Retired accounts (${retired.length})`));
707
751
  // A signature that fails is a warning, and a warning is not left behind a fold.
708
- history.append(count, ...cards(retired, () => {
752
+ const gone = node('div', '', 'tiles');
753
+ gone.append(...cards(retired, () => {
709
754
  history.open = true;
710
755
  }));
756
+ history.append(count, gone);
711
757
  // Shut to keep history out of the way of the accounts in use. With none of those there
712
758
  // is nothing else to read, so it starts open.
713
759
  history.open = folds.retired ?? !listed.length;
@@ -715,11 +761,16 @@ function render(content, records, manage, later, folds = { open: false, cards: n
715
761
  // group turns, so what it will be is the opposite of what it is.
716
762
  count.addEventListener('click', () => {
717
763
  folds.retired = !history.open;
764
+ if (folds.retired)
765
+ reveal(history);
718
766
  });
719
- content.replaceChildren(localCard, linkMark(), ...cards(listed), ...(manage ? [addRow(manage)] : []), ...(retired.length ? [history] : []),
767
+ const accounts = node('div', '', 'tiles');
768
+ accounts.append(...cards(listed));
769
+ content.replaceChildren(localCard, linkMark(), accounts, ...(manage ? [addRow(manage)] : []), ...(retired.length ? [history] : []),
720
770
  // Nothing below the cards may name a provider. Approval, method and dates belong to
721
771
  // the card they came from, and a second provider on this subject gets its own card.
722
772
  node('p', 'Verification does not guarantee legal identity, trustworthiness, or permanent ownership.', 'muted explanation'));
773
+ return (listed.length || retired.length) >= 4;
723
774
  }
724
775
  /**
725
776
  * Fetch fresh, permitted evidence; native dialog supplies focus containment and Escape
@@ -766,6 +817,7 @@ detailsOpen = false) {
766
817
  // What drew the record, under it. The record is about the pair, not about us.
767
818
  const stamp = node('footer');
768
819
  const content = node('div', 'Checking verification…');
820
+ const pane = node('div', '', 'pane');
769
821
  heading.id = 'verily-dialog-title';
770
822
  dialog.setAttribute('aria-labelledby', heading.id);
771
823
  close.type = 'button';
@@ -778,7 +830,8 @@ detailsOpen = false) {
778
830
  close.autofocus = true;
779
831
  title.append(heading);
780
832
  header.append(title, close);
781
- dialog.append(header, content, stamp);
833
+ pane.append(header, content, stamp);
834
+ dialog.append(pane);
782
835
  /** Links removed from this dialog, shown as removed before the page hands over new records. */
783
836
  const removed = new Map();
784
837
  /**
@@ -891,7 +944,8 @@ detailsOpen = false) {
891
944
  drawn = key;
892
945
  // The cards are new, and what the old ones were waiting to try again went with them.
893
946
  retries.clear();
894
- render(content, records, actions, (retry) => retries.add(retry), folds);
947
+ const many = render(content, records, actions, (retry) => retries.add(retry), folds);
948
+ dialog.className = many ? 'many' : '';
895
949
  };
896
950
  /**
897
951
  * Redraws at the moment the next record runs out, where that comes before the next check
@@ -386,7 +386,12 @@ function accountCard(e, external, now) {
386
386
  <p class="how">
387
387
  ${verdict(toneOf(e, now))}${statusLabel(e, now)} · via: <a href="${record}">${e.verifierName}</a>${e.signedUrl && html `<a class="signed-mark" href="${record}#signed">signed</a>`}
388
388
  </p>
389
- `, externalNotes({ ...e.attestations, external }, { site: e.siteName, provider: e.providerName }), times(retired
389
+ `,
390
+ // Everything under the verdict in one part, the third of the card's rows in a grid.
391
+ html `
392
+ <div class="rest">
393
+ ${externalNotes({ ...e.attestations, external }, { site: e.siteName, provider: e.providerName })}
394
+ ${times(retired
390
395
  ? [
391
396
  ['Approved', e.approvedAt],
392
397
  ['Last verified', lastProved(e)],
@@ -396,7 +401,9 @@ function accountCard(e, external, now) {
396
401
  ['Approved', e.approvedAt],
397
402
  [e.expiresAt > now ? 'Valid until' : 'Expired on', e.expiresAt],
398
403
  ['Last checked', external[0].artifactUrl ? external[0].confirmedAt : undefined],
399
- ]),
404
+ ])}
405
+ </div>
406
+ `,
400
407
  // The holder's own word on the account, which nothing checked, so it sits apart from
401
408
  // the methods, in the card's corner as the dialog has it.
402
409
  !retired &&
@@ -421,7 +428,9 @@ function publishedPage(records, base) {
421
428
  ${[...subjects.values()].map((held) => {
422
429
  const local = localSide(held[0].local, held[0].siteName);
423
430
  return html `
424
- ${card(local.heading, local.value, held[0].local.profileUrl)}
431
+ <div class="subject">
432
+ ${card(local.heading, local.value, held[0].local.profileUrl)}
433
+ </div>
425
434
  ${joiner}
426
435
  <div class="accounts">
427
436
  ${accountsOf(held, now).map(({ lead, external }) => accountCard(lead, external, now))}
@@ -88,18 +88,20 @@ export function side(heading, name, url, reference, since, extra) {
88
88
  const profile = url && safeUrl(url);
89
89
  return html `
90
90
  <div class="side">
91
- <p class="who">${typeof heading === 'string' ? heading : [heading.mark, heading.text]}</p>
92
- <p class="name">
93
- ${profile ? html `<a href="${profile}" rel="noreferrer">${name}</a>` : name}${since && html `<span class="since">${since}</span>`}
94
- </p>
95
- ${typeof reference === 'string'
91
+ <div class="identity">
92
+ <p class="who">${typeof heading === 'string' ? heading : [heading.mark, heading.text]}</p>
93
+ <p class="name">
94
+ ${profile ? html `<a href="${profile}" rel="noreferrer">${name}</a>` : name}${since && html `<span class="since">${since}</span>`}
95
+ </p>
96
+ ${typeof reference === 'string'
96
97
  ? html `<p class="reference">${reference}</p>`
97
98
  : reference &&
98
99
  html `
99
- <p class="reference fingerprint">
100
- ${reference.map((group) => html `<span>${group}</span>`)}
101
- </p>
102
- `}
100
+ <p class="reference fingerprint">
101
+ ${reference.map((group) => html `<span>${group}</span>`)}
102
+ </p>
103
+ `}
104
+ </div>
103
105
  ${extra}
104
106
  </div>
105
107
  `;
@@ -8,7 +8,7 @@
8
8
  * leaves. So the styling does the two things that helps with, a measured column and a
9
9
  * legible hierarchy, and nothing that would make a record look like a product.
10
10
  */
11
- export declare const stylesheet = ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\n color-scheme: light dark;\n accent-color: var(--accent);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #dde5e0;\n --muted: #96a39b;\n --line: #2f3b35;\n --surface: #161a18;\n --raised: #1d2220;\n --accent: #7fc4a2;\n --warn: #e59a82;\n --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font: 16px/1.65 system-ui, -apple-system, 'Segoe UI', sans-serif;\n text-rendering: optimizeLegibility;\n}\n\nmain {\n max-width: 34rem;\n margin: 0 auto;\n padding-top: 3rem;\n}\n\n/*\n * The logotype, above the page's own heading, on its own dark plate. Sized so the word is\n * the height it was without the plate, whose margin makes the box that much taller. The\n * word is white in either scheme: the plate is what it is read against, not the page.\n */\n.logo {\n display: block;\n height: 2.5rem;\n margin-bottom: 1.5rem;\n}\n\nh1 {\n margin: 0 0 1.75rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--line);\n font-size: 1.5rem;\n font-weight: 620;\n letter-spacing: -.02em;\n line-height: 1.25;\n}\n\n/*\n * A page holding one thing needs no rule under its heading. The rule divides a heading\n * from the record beneath it, and a page that only asks for a key has no record to divide.\n */\nmain.single h1 { margin-bottom: 1.25rem; padding-bottom: 0; border-bottom: none; }\n\n/*\n * A page holding one thing sits in the middle of the window rather than at its top,\n * where it would leave most of the window empty below it.\n */\nbody:has(main.single) { display: flex; min-height: 100vh; min-height: 100dvh; }\nmain.single { width: 100%; max-width: 24rem; margin: auto; padding-bottom: 3rem; }\n\nh2 { margin: 2rem 0 .75rem; font-size: 1.125rem; font-weight: 620; letter-spacing: -.01em; }\n\nh3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 620; }\n\np { margin: 0 0 1rem; overflow-wrap: break-word; }\n\na { color: var(--accent); text-underline-offset: 3px; overflow-wrap: anywhere; }\n\na:hover { text-decoration-thickness: 2px; }\n\n/*\n * Something to be run or published exactly as written. Its own line breaks are kept and a\n * long line is soft-wrapped rather than cut off: a soft wrap is drawn, not inserted, so\n * what is copied is still character for character what was written. Selecting inside the\n * block takes all of it, since half a command is worse than none.\n */\npre {\n position: relative;\n margin: 0 0 1rem;\n /* Room along the top line for the copy button, which sits over the block rather than in it. */\n padding: .75rem 5.25rem .75rem .875rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--raised);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n user-select: all;\n}\n\ncode { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag { color: var(--muted); }\n.hljs-name { color: var(--syntax-tag); }\n.hljs-attr { color: var(--syntax-attr); }\n.hljs-string { color: var(--syntax-string); }\n\n/* Made by a script, so it exists only where it works. Quiet: the block is the point. */\n.copy {\n position: absolute;\n top: .5rem;\n right: .5rem;\n padding: .1875rem .5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font: 500 .75rem/1.5 system-ui, sans-serif;\n user-select: none;\n}\n\n.copy:hover { color: var(--ink); border-color: var(--muted); }\n\n/*\n * The two sides of the link, which is what a reader came to read. Each fact about a side\n * gets its own line: what it is, what it is called, the identifier behind that, and how it\n * was shown. Everything else on the page qualifies the pair, so it is set quieter.\n */\n.side {\n margin-bottom: .75rem;\n padding: .875rem 1rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--raised);\n}\n\n/*\n * A subject's accounts are peers, so where there is room they sit two across. The page\n * that lists them is wider for it, and a narrow one keeps them in a single column.\n */\nmain:has(.accounts) { max-width: 54rem; }\n\n.accounts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));\n gap: .75rem;\n margin-bottom: .75rem;\n}\n\n.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }\n\n/* How the holder lists the account, in the card's corner and out of the lines below. */\n.listing {\n position: absolute;\n top: .75rem;\n right: .875rem;\n padding: .0625rem .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: .6875rem;\n font-weight: 550;\n}\n\n.listing.preferred { border-color: var(--accent); color: var(--accent); }\n\n/* The times sit at the foot of the card, so they line up along a row of uneven cards. */\n.accounts .side dl { margin-top: auto; }\n.accounts .side p:last-of-type { margin-bottom: .625rem; }\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }\n\n.who {\n margin: 0 0 .125rem;\n color: var(--muted);\n font-size: .75rem;\n font-weight: 550;\n letter-spacing: .02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider { display: inline-block; width: .875rem; height: .875rem; margin-right: .375rem; vertical-align: -.125rem; }\n\n/* Clipped 4px left of the text, which hides the date's dot when the date wraps to a new line. */\n.name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1rem; margin: 0 0 0 -4px; padding-left: 4px; overflow-x: clip; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since { position: relative; color: var(--muted); font-size: .8125rem; font-weight: 400; letter-spacing: 0; white-space: nowrap; }\n\n/* The dot sits in the gap before the date. */\n.name .since::before { content: \"\u00B7\"; position: absolute; left: -.5rem; transform: translateX(-50%); }\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a { text-decoration: none; }\n\n.name a:hover { text-decoration: underline; }\n\n.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }\n\n.name + .how, .reference + .how { margin-top: .5rem; }\n.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n.fingerprint span { display: inline-block; margin-right: .5ch; }\n.how.additional { padding-left: .75rem; }\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark { display: inline-block; margin-left: .3125rem; padding: 0 .375rem; border: 1px solid var(--line); border-radius: 999px; font-size: .625rem; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: .0625rem; }\n\n.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }\n\n/* That the record is signed is a fact about it, so it is set as one and not as fine print. */\n.signed { font-size: .875rem; line-height: 1.55; margin-bottom: .875rem; }\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target { margin-inline: -.75rem; padding: .625rem .75rem; border-radius: 8px; background: var(--raised); }\n\n/* Every time on the record in one column, so they can be compared rather than hunted for. */\ndl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: .3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: .8125rem;\n}\n\ndt { color: var(--muted); }\n\ndd { margin: 0; font-variant-numeric: tabular-nums; }\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl { padding-top: 0; border-top: 0; }\n.side dl { margin: .625rem 0 0; padding-top: .625rem; }\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }\nfooter > :last-child { margin-bottom: 0; }\n\nsection {\n margin: 0 0 1.25rem;\n padding: 1.25rem 1.375rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n\nsection > :last-child { margin-bottom: 0; }\n\n/* What holds a control that cannot be undone. */\nsection.danger, section.caution { margin-top: 2rem; }\nsection.danger { border-color: var(--warn); }\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution { border-color: var(--caution); }\nsection > h2:first-child { margin-top: 0; }\n\nform { margin: 0 0 1.5rem; }\n\nlabel { display: block; margin: 0 0 .75rem; color: var(--muted); font-size: .8125rem; }\n\ninput, textarea {\n display: block;\n width: 100%;\n margin-top: .375rem;\n padding: .5rem .625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea { resize: vertical; }\n\ninput:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\nbutton {\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font: 500 .9375rem/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;\n cursor: pointer;\n}\n\n/* A link that starts something, drawn as a button. */\na.button {\n display: inline-block;\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font-weight: 500;\n font-size: .9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary { border-color: var(--line); background: none; color: var(--ink); }\n\n/* A page's action and its alternative, side by side. */\n.choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }\n.choice form { margin: 0; }\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices { display: grid; gap: .5rem; margin-bottom: 1.5rem; }\n.choices form { margin: 0; }\n\n.choices button {\n width: 100%;\n text-align: left;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n}\n\n.choices button:hover { border-color: var(--muted); }\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider { width: 1rem; height: 1rem; margin-right: .5rem; vertical-align: -.1875rem; }\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger { border-color: var(--warn); background: var(--warn); }\n\n/*\n * A connection on the settings page: the account's card, then what can be done with it.\n * Its state sits in the card's corner, on the line that names the method.\n */\n.connection { position: relative; margin-bottom: 1rem; }\n\n.state {\n position: absolute;\n top: .75rem;\n right: .875rem;\n display: flex;\n align-items: center;\n gap: .375rem;\n height: 1.625rem;\n padding: 0 .75rem 0 .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n font-weight: 550;\n}\n\n/*\n * The V the badge draws, in the tone that says how the record stands. A record that claims\n * nothing is drawn faint in the text colour, as the badge draws it.\n */\n.verdict { flex-shrink: 0; width: 1rem; height: 1rem; }\n.verdict.pending { opacity: .45; }\n.how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) { display: flex; align-items: center; gap: .75rem; }\nh1 .verdict { width: auto; height: 2.25rem; }\n\n/*\n * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms\n * and its foot meet the border. The pill's left padding keeps the arms clear of its curve.\n */\n.state .verdict { width: auto; height: 100%; }\n\n/*\n * A pill's line includes room for descenders below the letters, which leaves its text\n * sitting high. Trimmed to the letters, the padding alone sets the space above and below.\n * In a state pill the word alone is trimmed, so it centres on the V by its letters.\n */\n@supports (text-box: trim-both cap alphabetic) {\n .listing { padding: .3125rem .5rem; text-box: trim-both cap alphabetic; }\n .state > span { text-box: trim-both cap alphabetic; }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; font-size: .875rem; }\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence { margin: .375rem 0 0; font-size: .8125rem; }\n\n.connection dl { margin-bottom: .75rem; }\n.connection .side > :last-child { margin-bottom: 0; }\n.connection pre { margin: .75rem 0; background: var(--surface); }\n.connection .actions { margin-top: .875rem; }\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n.actions form { display: contents; }\n\n.actions button, .actions a.button {\n margin: 0;\n padding: .3125rem .75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n}\n\n.actions button:hover, .actions a.button:hover { border-color: var(--muted); }\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line); }\n.ending form { margin: 0; }\n\n.ending button.danger {\n padding: .3125rem .75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: .8125rem;\n}\n\nfieldset { margin: 0 0 1.25rem; padding: 1rem 1.125rem .25rem; border: 1px solid var(--line); border-radius: 8px; }\n\nlegend { padding: 0 .375rem; color: var(--muted); font-size: .8125rem; }\n\nfieldset label { display: flex; align-items: baseline; gap: .5rem; color: var(--ink); font-size: .9375rem; }\n\nfieldset input { width: auto; margin: 0; }\n\nfieldset p { margin: .25rem 0 .875rem 1.5rem; color: var(--muted); font-size: .8125rem; }\n";
11
+ export declare const stylesheet = ":root {\n --ink: #23312b;\n --muted: #6b786f;\n --line: #dce2de;\n --surface: #fff;\n --raised: #f7f9f7;\n --accent: #245f43;\n --warn: #a4442b;\n --caution: #b07a12;\n --syntax-tag: #245f43;\n --syntax-attr: #8a5a0b;\n --syntax-string: #2b5f8a;\n color-scheme: light dark;\n accent-color: var(--accent);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #dde5e0;\n --muted: #96a39b;\n --line: #2f3b35;\n --surface: #161a18;\n --raised: #1d2220;\n --accent: #7fc4a2;\n --warn: #e59a82;\n --caution: #d9a843;\n --syntax-tag: #7fc4a2;\n --syntax-attr: #d9a843;\n --syntax-string: #8fbbe0;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n padding: 0 1rem 4rem;\n background: var(--surface);\n color: var(--ink);\n font: 16px/1.65 system-ui, -apple-system, 'Segoe UI', sans-serif;\n text-rendering: optimizeLegibility;\n}\n\nmain {\n max-width: 34rem;\n margin: 0 auto;\n padding-top: 3rem;\n}\n\n/*\n * The logotype, above the page's own heading, on its own dark plate. Sized so the word is\n * the height it was without the plate, whose margin makes the box that much taller. The\n * word is white in either scheme: the plate is what it is read against, not the page.\n */\n.logo {\n display: block;\n height: 2.5rem;\n margin-bottom: 1.5rem;\n}\n\nh1 {\n margin: 0 0 1.75rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--line);\n font-size: 1.5rem;\n font-weight: 620;\n letter-spacing: -.02em;\n line-height: 1.25;\n}\n\n/*\n * A page holding one thing needs no rule under its heading. The rule divides a heading\n * from the record beneath it, and a page that only asks for a key has no record to divide.\n */\nmain.single h1 { margin-bottom: 1.25rem; padding-bottom: 0; border-bottom: none; }\n\n/*\n * A page holding one thing sits in the middle of the window rather than at its top,\n * where it would leave most of the window empty below it.\n */\nbody:has(main.single) { display: flex; min-height: 100vh; min-height: 100dvh; }\nmain.single { width: 100%; max-width: 24rem; margin: auto; padding-bottom: 3rem; }\n\nh2 { margin: 2rem 0 .75rem; font-size: 1.125rem; font-weight: 620; letter-spacing: -.01em; }\n\nh3 { margin: 0 0 .5rem; font-size: 1rem; font-weight: 620; }\n\np { margin: 0 0 1rem; overflow-wrap: break-word; }\n\na { color: var(--accent); text-underline-offset: 3px; overflow-wrap: anywhere; }\n\na:hover { text-decoration-thickness: 2px; }\n\n/*\n * Something to be run or published exactly as written. Its own line breaks are kept and a\n * long line is soft-wrapped rather than cut off: a soft wrap is drawn, not inserted, so\n * what is copied is still character for character what was written. Selecting inside the\n * block takes all of it, since half a command is worse than none.\n */\npre {\n position: relative;\n margin: 0 0 1rem;\n /* Room along the top line for the copy button, which sits over the block rather than in it. */\n padding: .75rem 5.25rem .75rem .875rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--raised);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n user-select: all;\n}\n\ncode { font: .8125rem/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }\n\n/* Highlighted HTML, marked up by highlight.js. Brackets stay muted so the names read first. */\n.hljs-tag { color: var(--muted); }\n.hljs-name { color: var(--syntax-tag); }\n.hljs-attr { color: var(--syntax-attr); }\n.hljs-string { color: var(--syntax-string); }\n\n/* Made by a script, so it exists only where it works. Quiet: the block is the point. */\n.copy {\n position: absolute;\n top: .5rem;\n right: .5rem;\n padding: .1875rem .5rem;\n border: 1px solid var(--line);\n border-radius: 5px;\n background: var(--surface);\n color: var(--muted);\n font: 500 .75rem/1.5 system-ui, sans-serif;\n user-select: none;\n}\n\n.copy:hover { color: var(--ink); border-color: var(--muted); }\n\n/*\n * The two sides of the link, which is what a reader came to read. Each fact about a side\n * gets its own line: what it is, what it is called, the identifier behind that, and how it\n * was shown. Everything else on the page qualifies the pair, so it is set quieter.\n */\n.side {\n margin-bottom: .75rem;\n padding: .875rem 1rem;\n border: 1px solid var(--line);\n border-radius: 8px;\n background: var(--raised);\n}\n\n/*\n * A subject's accounts are peers, so where there is room they sit two across. The page\n * that lists them is wider for it, and a narrow one keeps them in a single column.\n */\nmain:has(.accounts) { max-width: 54rem; }\n\n.accounts {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: .75rem;\n margin-bottom: .75rem;\n}\n\n.accounts .side { position: relative; display: flex; flex-direction: column; margin-bottom: 0; }\n\n/* How the holder lists the account, in the card's corner and out of the lines below. */\n.listing {\n position: absolute;\n top: .75rem;\n right: .875rem;\n padding: .0625rem .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n color: var(--muted);\n font-size: .6875rem;\n font-weight: 550;\n}\n\n.listing.preferred { border-color: var(--accent); color: var(--accent); }\n\n/* The times sit at the foot of the card, so they line up along a row of uneven cards. */\n.accounts .side dl { margin-top: auto; }\n.accounts .rest { display: flex; flex-direction: column; }\n.accounts .rest > p:last-of-type { margin-bottom: .625rem; }\n\n/*\n * Two across, with the subject one card wide and centred above them. Every card on the\n * page is then the same width.\n */\n@media (min-width: 48rem) {\n .accounts { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall\n as the taller card's part, so the verdicts and times of neighbouring cards line up. */\n .accounts > .side { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; }\n .subject { width: calc(50% - .375rem); margin-inline: auto; text-align: center; }\n .subject .name { justify-content: center; }\n}\n\n/* Between a subject and its accounts on the page that lists them. */\n.joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }\n\n.who {\n margin: 0 0 .125rem;\n color: var(--muted);\n font-size: .75rem;\n font-weight: 550;\n letter-spacing: .02em;\n}\n\n/* A provider's mark before its name, at the height of the letters. */\n.who .provider { display: inline-block; width: .875rem; height: .875rem; margin-right: .375rem; vertical-align: -.125rem; }\n\n/* Clipped 4px left of the text, which hides the date's dot when the date wraps to a new line. */\n.name { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1rem; margin: 0 0 0 -4px; padding-left: 4px; overflow-x: clip; font-size: 1.0625rem; font-weight: 620; letter-spacing: -.01em; }\n\n/* The account's creation date, after the name and a dot. It wraps under a long name. */\n.name .since { position: relative; color: var(--muted); font-size: .8125rem; font-weight: 400; letter-spacing: 0; white-space: nowrap; }\n\n/* The dot sits in the gap before the date. */\n.name .since::before { content: \"\u00B7\"; position: absolute; left: -.5rem; transform: translateX(-50%); }\n\n/* The name is the card's subject before it is a link, so the rule is drawn on hover. */\n.name a { text-decoration: none; }\n\n.name a:hover { text-decoration: underline; }\n\n.reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }\n\n.identity + .how { margin-top: .5rem; }\n.fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }\n.fingerprint span { display: inline-block; margin-right: .5ch; }\n.how.additional { padding-left: .75rem; }\n/* The \"signed\" pill after the verifier's name, one size smaller than the line it sits on. */\n.how .signed-mark { display: inline-block; margin-left: .3125rem; padding: 0 .375rem; border: 1px solid var(--line); border-radius: 999px; font-size: .625rem; font-weight: 550; line-height: 1.3; text-decoration: none; vertical-align: .0625rem; }\n\n.fine { color: var(--muted); font-size: .8125rem; line-height: 1.55; margin-bottom: .625rem; }\n\n/* That the record is signed is a fact about it, so it is set as one and not as fine print. */\n.signed { font-size: .875rem; line-height: 1.55; margin-bottom: .875rem; }\n/* Arrived at from a badge's \"signed\" link, so the reader is shown where they landed. */\n.signed:target { margin-inline: -.75rem; padding: .625rem .75rem; border-radius: 8px; background: var(--raised); }\n\n/* Every time on the record in one column, so they can be compared rather than hunted for. */\ndl {\n display: grid;\n grid-template-columns: auto 1fr;\n gap: .3rem 1.5rem;\n margin: 1.25rem 0;\n padding-top: 1rem;\n border-top: 1px solid var(--line);\n font-size: .8125rem;\n}\n\ndt { color: var(--muted); }\n\ndd { margin: 0; font-variant-numeric: tabular-nums; }\n\n/* Inside a card the rule above it is the card's own edge, so the list needs none. */\nsection dl { margin: .5rem 0 1rem; padding-top: 0; border-top: 0; }\n\n/* Right under the heading, the heading's own rule is the one above it. */\nh1 + dl { padding-top: 0; border-top: 0; }\n.side dl { margin: .625rem 0 0; padding-top: .625rem; }\n\n/* The links under a record, ruled off from the record and its disclaimer. */\nfooter { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }\nfooter > :last-child { margin-bottom: 0; }\n\nsection {\n margin: 0 0 1.25rem;\n padding: 1.25rem 1.375rem;\n border: 1px solid var(--line);\n border-radius: 10px;\n}\n\nsection > :last-child { margin-bottom: 0; }\n\n/* What holds a control that cannot be undone. */\nsection.danger, section.caution { margin-top: 2rem; }\nsection.danger { border-color: var(--warn); }\n\n/* What holds a control that breaks something for a while, but can be recovered from. */\nsection.caution { border-color: var(--caution); }\nsection > h2:first-child { margin-top: 0; }\n\nform { margin: 0 0 1.5rem; }\n\nlabel { display: block; margin: 0 0 .75rem; color: var(--muted); font-size: .8125rem; }\n\ninput, textarea {\n display: block;\n width: 100%;\n margin-top: .375rem;\n padding: .5rem .625rem;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--surface);\n color: var(--ink);\n font: .875rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n/* Height comes from the rows attribute, which differs per box. */\ntextarea { resize: vertical; }\n\ninput:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }\n\nbutton {\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font: 500 .9375rem/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif;\n cursor: pointer;\n}\n\n/* A link that starts something, drawn as a button. */\na.button {\n display: inline-block;\n padding: .5rem 1rem;\n border: 1px solid var(--accent);\n border-radius: 6px;\n background: var(--accent);\n color: var(--surface);\n font-weight: 500;\n font-size: .9375rem;\n line-height: 1.4;\n text-decoration: none;\n}\n\nbutton + button { margin-left: .5rem; border-color: var(--line); background: none; color: var(--ink); }\n\n/* The other choice beside a page's one action, such as Cancel. */\na.button.secondary { border-color: var(--line); background: none; color: var(--ink); }\n\n/* A page's action and its alternative, side by side. */\n.choice { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.5rem; }\n.choice form { margin: 0; }\n\n/* Ways to verify: one full-width button per method, none filled over the others. */\n.choices { display: grid; gap: .5rem; margin-bottom: 1.5rem; }\n.choices form { margin: 0; }\n\n.choices button {\n width: 100%;\n text-align: left;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n}\n\n.choices button:hover { border-color: var(--muted); }\n\n/* A provider's mark before a button's label, at the height of the letters. */\nbutton .provider { width: 1rem; height: 1rem; margin-right: .5rem; vertical-align: -.1875rem; }\n\n/* A control that ends something. Filled where it is the page's one action. */\nbutton.danger { border-color: var(--warn); background: var(--warn); }\n\n/*\n * A connection on the settings page: the account's card, then what can be done with it.\n * Its state sits in the card's corner, on the line that names the method.\n */\n.connection { position: relative; margin-bottom: 1rem; }\n\n.state {\n position: absolute;\n top: .75rem;\n right: .875rem;\n display: flex;\n align-items: center;\n gap: .375rem;\n height: 1.625rem;\n padding: 0 .75rem 0 .5rem;\n border: 1px solid var(--line);\n border-radius: 999px;\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n font-weight: 550;\n}\n\n/*\n * The V the badge draws, in the tone that says how the record stands. A record that claims\n * nothing is drawn faint in the text colour, as the badge draws it.\n */\n.verdict { flex-shrink: 0; width: 1rem; height: 1rem; }\n.verdict.pending { opacity: .45; }\n.how .verdict { width: .875rem; height: .875rem; margin-right: .3125rem; vertical-align: -.125rem; }\n/* Cropped to its strokes and taller than the heading's letters, centred beside them. */\nh1:has(.verdict) { display: flex; align-items: center; gap: .75rem; }\nh1 .verdict { width: auto; height: 2.25rem; }\n\n/*\n * In a state pill the V is cropped to its strokes and fills the pill's height, so its arms\n * and its foot meet the border. The pill's left padding keeps the arms clear of its curve.\n */\n.state .verdict { width: auto; height: 100%; }\n\n/*\n * A pill's line includes room for descenders below the letters, which leaves its text\n * sitting high. Trimmed to the letters, the padding alone sets the space above and below.\n * In a state pill the word alone is trimmed, so it centres on the V by its letters.\n */\n@supports (text-box: trim-both cap alphabetic) {\n .listing { padding: .3125rem .5rem; text-box: trim-both cap alphabetic; }\n .state > span { text-box: trim-both cap alphabetic; }\n}\n\n/* Links that sit together on one line, wrapping where there is no room. */\n.links { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.25rem; font-size: .875rem; }\n\n/* Under the account's id, with the facts about it rather than the controls. */\n.evidence { margin: .375rem 0 0; font-size: .8125rem; }\n\n.connection dl { margin-bottom: .75rem; }\n.connection .side > :last-child { margin-bottom: 0; }\n.connection pre { margin: .75rem 0; background: var(--surface); }\n.connection .actions { margin-top: .875rem; }\n\n/* The buttons for one connection in a row, wrapping where there is no room. */\n.actions { display: flex; flex-wrap: wrap; gap: .5rem; }\n.actions form { display: contents; }\n\n.actions button, .actions a.button {\n margin: 0;\n padding: .3125rem .75rem;\n border-color: var(--line);\n background: var(--surface);\n color: var(--ink);\n font-size: .8125rem;\n}\n\n.actions button:hover, .actions a.button:hover { border-color: var(--muted); }\n\n/* The controls that cannot be undone, together at the foot of the card. */\n.ending { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .5rem; margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--line); }\n.ending form { margin: 0; }\n\n.ending button.danger {\n padding: .3125rem .75rem;\n background: var(--surface);\n color: var(--warn);\n font-size: .8125rem;\n}\n\nfieldset { margin: 0 0 1.25rem; padding: 1rem 1.125rem .25rem; border: 1px solid var(--line); border-radius: 8px; }\n\nlegend { padding: 0 .375rem; color: var(--muted); font-size: .8125rem; }\n\nfieldset label { display: flex; align-items: baseline; gap: .5rem; color: var(--ink); font-size: .9375rem; }\n\nfieldset input { width: auto; margin: 0; }\n\nfieldset p { margin: .25rem 0 .875rem 1.5rem; color: var(--muted); font-size: .8125rem; }\n";
12
12
  /**
13
13
  * A short digest of the sheet above, put in the link's query so a release that changes the
14
14
  * styling changes its address. Without that the only safe cache is no cache: a reader who
@@ -165,7 +165,7 @@ main:has(.accounts) { max-width: 54rem; }
165
165
 
166
166
  .accounts {
167
167
  display: grid;
168
- grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
168
+ grid-template-columns: minmax(0, 1fr);
169
169
  gap: .75rem;
170
170
  margin-bottom: .75rem;
171
171
  }
@@ -189,7 +189,21 @@ main:has(.accounts) { max-width: 54rem; }
189
189
 
190
190
  /* The times sit at the foot of the card, so they line up along a row of uneven cards. */
191
191
  .accounts .side dl { margin-top: auto; }
192
- .accounts .side p:last-of-type { margin-bottom: .625rem; }
192
+ .accounts .rest { display: flex; flex-direction: column; }
193
+ .accounts .rest > p:last-of-type { margin-bottom: .625rem; }
194
+
195
+ /*
196
+ * Two across, with the subject one card wide and centred above them. Every card on the
197
+ * page is then the same width.
198
+ */
199
+ @media (min-width: 48rem) {
200
+ .accounts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
201
+ /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall
202
+ as the taller card's part, so the verdicts and times of neighbouring cards line up. */
203
+ .accounts > .side { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; }
204
+ .subject { width: calc(50% - .375rem); margin-inline: auto; text-align: center; }
205
+ .subject .name { justify-content: center; }
206
+ }
193
207
 
194
208
  /* Between a subject and its accounts on the page that lists them. */
195
209
  .joiner { display: block; width: 1.25rem; height: 1.25rem; margin: 0 auto .75rem; color: var(--muted); }
@@ -221,7 +235,7 @@ main:has(.accounts) { max-width: 54rem; }
221
235
 
222
236
  .reference, .how { margin: 0; color: var(--muted); font-size: .8125rem; line-height: 1.5; }
223
237
 
224
- .name + .how, .reference + .how { margin-top: .5rem; }
238
+ .identity + .how { margin-top: .5rem; }
225
239
  .fingerprint { font-family: ui-monospace, monospace; text-wrap: balance; }
226
240
  .fingerprint span { display: inline-block; margin-right: .5ch; }
227
241
  .how.additional { padding-left: .75rem; }
package/dist/verily.js CHANGED
@@ -10520,7 +10520,7 @@ ${claims(evidence)}`;
10520
10520
  }
10521
10521
 
10522
10522
  // src/version.ts
10523
- var version = "v0.9.2";
10523
+ var version = "v0.9.4";
10524
10524
 
10525
10525
  // src/browser/evidence-dialog.ts
10526
10526
  function stampLink() {
@@ -10531,7 +10531,10 @@ ${claims(evidence)}`;
10531
10531
  var openDialogs = /* @__PURE__ */ new WeakMap();
10532
10532
  var styles2 = `
10533
10533
  * { box-sizing: border-box; }
10534
- dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 24px; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }
10534
+ dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); margin: auto; padding: 0; overflow: hidden; border: 1px solid #dce2de; border-radius: 16px; background: #fff; color: #23312b; box-shadow: 0 24px 90px #10201935; font: 13px/1.6 system-ui, sans-serif; }
10535
+ /* The pane scrolls inside the dialog, which clips its scrollbar to the rounded corners. */
10536
+ dialog[open] { display: flex; flex-direction: column; }
10537
+ .pane { min-height: 0; padding: 24px; overflow-y: auto; }
10535
10538
  dialog::backdrop { background: #15271f66; }
10536
10539
  header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
10537
10540
  h2 { margin: 0; font-size: 18px; font-weight: 650; letter-spacing: -.3px; }
@@ -10579,7 +10582,7 @@ ${claims(evidence)}`;
10579
10582
  .listing.preferred { border-color: #245f43; color: #245f43; }
10580
10583
  /* Quiet, as its pill is: the surface and the name step back, and the controls do not. */
10581
10584
  .account.retired { background: #f6f8f5; }
10582
- .account.retired > a, .account.retired > strong, .account.retired > .named > a, .account.retired > .named > strong { color: #6b786f; }
10585
+ .account.retired .identity > a, .account.retired .identity > strong, .account.retired .named > a, .account.retired .named > strong { color: #6b786f; }
10583
10586
  /* The name, then a dot and the account's creation date. The date wraps under a long name. */
10584
10587
  /* Clipped 4px left of the text, which hides the dot when the date wraps to a new line. */
10585
10588
  .named { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; margin-left: -4px; padding-left: 4px; overflow-x: clip; }
@@ -10622,6 +10625,22 @@ ${claims(evidence)}`;
10622
10625
  footer { display: flex; align-items: center; justify-content: start; gap: 6px; margin: 20px 0 -8px; color: #9aa9a0; font-size: 11px; }
10623
10626
  footer a { display: flex; color: inherit; }
10624
10627
  .logo { display: block; height: 13px; }
10628
+ /* Four or more accounts on a wide screen: two equal cards to a row. */
10629
+ @media (min-width: 960px) {
10630
+ dialog.many { width: min(900px, calc(100vw - 32px)); }
10631
+ dialog.many .tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 12px; }
10632
+ /* Each card is three rows of the grid: who, its verdict, and the rest. A row is as tall as
10633
+ the taller card's part, so the verdicts and details of neighbouring cards line up. */
10634
+ dialog.many .tiles > .account { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-top: 0; }
10635
+ /* The dates sit at the foot of the third part, so they line up too. */
10636
+ dialog.many .tiles .rest { display: flex; flex-direction: column; }
10637
+ dialog.many .tiles .rest > dl { margin-top: auto; }
10638
+ dialog.many .tiles .rest > :has(+ dl) { margin-bottom: 16px; }
10639
+ /* The site's card is one tile wide, centred over the two columns. */
10640
+ dialog.many .local { width: calc(50% - 6px); margin-inline: auto; text-align: center; }
10641
+ dialog.many .local h3, dialog.many .local .named { justify-content: center; }
10642
+ dialog.many .local .additional { padding-left: 0; }
10643
+ }
10625
10644
  ${markStyles}
10626
10645
  `;
10627
10646
  function node(tag, text = "", className = "") {
@@ -10746,6 +10765,7 @@ ${claims(evidence)}`;
10746
10765
  }
10747
10766
  function side(heading, name, reference, url, since, extra) {
10748
10767
  const card = node("section", "", "account");
10768
+ const identity = node("div", "", "identity");
10749
10769
  const title = node("h3");
10750
10770
  const value = node(url ? "a" : "strong", name);
10751
10771
  title.append(...heading);
@@ -10753,15 +10773,15 @@ ${claims(evidence)}`;
10753
10773
  if (since) {
10754
10774
  const named = node("div", "", "named");
10755
10775
  named.append(value, node("span", since, "muted since"));
10756
- card.append(title, named);
10757
- } else card.append(title, value);
10758
- if (typeof reference === "string") card.append(node("div", reference, "muted reference"));
10776
+ identity.append(title, named);
10777
+ } else identity.append(title, value);
10778
+ if (typeof reference === "string") identity.append(node("div", reference, "muted reference"));
10759
10779
  else if (reference) {
10760
10780
  const groups = node("div", "", "muted reference fingerprint");
10761
10781
  groups.append(...reference.map((group) => node("span", group)));
10762
- card.append(groups);
10782
+ identity.append(groups);
10763
10783
  }
10764
- card.append(...extra);
10784
+ card.append(identity, ...extra);
10765
10785
  return card;
10766
10786
  }
10767
10787
  var retiringNote = "Retiring keeps this record as history. It will no longer read as verified, and who can read it cannot be changed afterwards.";
@@ -10799,7 +10819,7 @@ ${claims(evidence)}`;
10799
10819
  const { height, bottom } = menu.getBoundingClientRect();
10800
10820
  if (bottom > innerHeight - edge)
10801
10821
  menu.style.top = `${Math.max(edge, more.getBoundingClientRect().top - gap - height)}px`;
10802
- menu.closest("dialog")?.addEventListener("scroll", shut, { once: true });
10822
+ menu.closest(".pane")?.addEventListener("scroll", shut, { once: true });
10803
10823
  choices[0].focus();
10804
10824
  });
10805
10825
  menu.addEventListener("click", shut);
@@ -10955,6 +10975,17 @@ ${claims(evidence)}`;
10955
10975
  }
10956
10976
  return svg;
10957
10977
  }
10978
+ function reveal(element) {
10979
+ requestAnimationFrame(() => {
10980
+ const pane = element.closest(".pane");
10981
+ const tall = pane !== null && element.offsetHeight > pane.clientHeight;
10982
+ const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
10983
+ element.scrollIntoView({
10984
+ block: tall ? "start" : "nearest",
10985
+ behavior: still ? "auto" : "smooth"
10986
+ });
10987
+ });
10988
+ }
10958
10989
  function foldToggle(folded, fold) {
10959
10990
  const toggle = node("button", "", "fold");
10960
10991
  let shown3 = fold.open;
@@ -10969,6 +11000,7 @@ ${claims(evidence)}`;
10969
11000
  toggle.onclick = () => {
10970
11001
  set(!shown3);
10971
11002
  fold.toggle(shown3);
11003
+ if (shown3) reveal(folded.at(-1));
10972
11004
  };
10973
11005
  return toggle;
10974
11006
  }
@@ -11017,6 +11049,8 @@ ${claims(evidence)}`;
11017
11049
  ];
11018
11050
  if (fold) bar.append(foldToggle([...methods, dates], fold), ...row);
11019
11051
  const logo2 = providerMark(evidence.provider, evidence.attestations.external[0].method);
11052
+ const rest = node("div", "", "rest");
11053
+ rest.append(...fold ? [bar, ...methods, dates] : [...methods, dates, ...row]);
11020
11054
  const card = namedCard(
11021
11055
  // The mark and the name, or just the name. A mark that falls back to writing the name
11022
11056
  // would print it twice here, since this heading writes it either way.
@@ -11025,9 +11059,8 @@ ${claims(evidence)}`;
11025
11059
  // Status first, then how it was shown, then when. The proof explains the state
11026
11060
  // above it, so it cannot sit before that state has been given.
11027
11061
  summary,
11028
- ...fold ? [] : methods,
11029
11062
  ...evidence.status === "revoked" || retired ? [] : evidence.mark === "preferred" ? [node("div", "Preferred", "listing preferred")] : evidence.mark === "unused" ? [node("div", "No longer used", "listing")] : [],
11030
- ...fold ? [bar, ...methods, dates] : [dates, ...row]
11063
+ rest
11031
11064
  );
11032
11065
  const surface = retired ? "account retired" : "account";
11033
11066
  card.className = surface;
@@ -11071,6 +11104,7 @@ ${claims(evidence)}`;
11071
11104
  provider: first.providerName
11072
11105
  })
11073
11106
  );
11107
+ localCard.className += " local";
11074
11108
  const retired = records.filter((record2) => record2.status === "retired");
11075
11109
  const listed = records.filter((record2) => record2.status !== "retired");
11076
11110
  const cards = (among, warned) => among.map((record2) => {
@@ -11090,20 +11124,24 @@ ${record2.external.id}`;
11090
11124
  const history = node("details", "", "history");
11091
11125
  const count = node("summary", "", "group");
11092
11126
  count.append(foldMark(), document.createTextNode(`Retired accounts (${retired.length})`));
11093
- history.append(
11094
- count,
11127
+ const gone = node("div", "", "tiles");
11128
+ gone.append(
11095
11129
  ...cards(retired, () => {
11096
11130
  history.open = true;
11097
11131
  })
11098
11132
  );
11133
+ history.append(count, gone);
11099
11134
  history.open = folds.retired ?? !listed.length;
11100
11135
  count.addEventListener("click", () => {
11101
11136
  folds.retired = !history.open;
11137
+ if (folds.retired) reveal(history);
11102
11138
  });
11139
+ const accounts2 = node("div", "", "tiles");
11140
+ accounts2.append(...cards(listed));
11103
11141
  content.replaceChildren(
11104
11142
  localCard,
11105
11143
  linkMark(),
11106
- ...cards(listed),
11144
+ accounts2,
11107
11145
  ...manage ? [addRow(manage)] : [],
11108
11146
  ...retired.length ? [history] : [],
11109
11147
  // Nothing below the cards may name a provider. Approval, method and dates belong to
@@ -11114,6 +11152,7 @@ ${record2.external.id}`;
11114
11152
  "muted explanation"
11115
11153
  )
11116
11154
  );
11155
+ return (listed.length || retired.length) >= 4;
11117
11156
  }
11118
11157
  function openEvidenceDialog(opener, load2, opened, arrange2 = (records) => records, manage, detailsOpen = false) {
11119
11158
  const existing = openDialogs.get(opener);
@@ -11134,6 +11173,7 @@ ${record2.external.id}`;
11134
11173
  const header = node("header");
11135
11174
  const stamp = node("footer");
11136
11175
  const content = node("div", "Checking verification\u2026");
11176
+ const pane = node("div", "", "pane");
11137
11177
  heading.id = "verily-dialog-title";
11138
11178
  dialog.setAttribute("aria-labelledby", heading.id);
11139
11179
  close.type = "button";
@@ -11145,7 +11185,8 @@ ${record2.external.id}`;
11145
11185
  close.autofocus = true;
11146
11186
  title.append(heading);
11147
11187
  header.append(title, close);
11148
- dialog.append(header, content, stamp);
11188
+ pane.append(header, content, stamp);
11189
+ dialog.append(pane);
11149
11190
  const removed = /* @__PURE__ */ new Map();
11150
11191
  const listed = [];
11151
11192
  const listing = (record2) => JSON.stringify([record2.status, record2.mark, record2.retiredAt, record2.approvedAt]);
@@ -11232,7 +11273,8 @@ ${record2.external.id}`;
11232
11273
  if (key2 === drawn) return;
11233
11274
  drawn = key2;
11234
11275
  retries.clear();
11235
- render(content, records, actions, (retry) => retries.add(retry), folds);
11276
+ const many = render(content, records, actions, (retry) => retries.add(retry), folds);
11277
+ dialog.className = many ? "many" : "";
11236
11278
  };
11237
11279
  const watch = (given) => {
11238
11280
  clearTimeout(expiryTimer);
@@ -11385,6 +11427,7 @@ ${record2.external.id}`;
11385
11427
  const header = node("header");
11386
11428
  const stamp = node("footer");
11387
11429
  const content = node("div", "Loading\u2026", "steps");
11430
+ const pane = node("div", "", "pane");
11388
11431
  heading.id = "verily-connect-title";
11389
11432
  dialog.setAttribute("aria-labelledby", heading.id);
11390
11433
  close.type = "button";
@@ -11393,7 +11436,8 @@ ${record2.external.id}`;
11393
11436
  content.setAttribute("aria-live", "polite");
11394
11437
  stamp.append(stampLink(), node("span", version));
11395
11438
  header.append(heading, close);
11396
- dialog.append(header, content, stamp);
11439
+ pane.append(header, content, stamp);
11440
+ dialog.append(pane);
11397
11441
  root.append(dialog);
11398
11442
  document.body.append(host);
11399
11443
  openDialogs2.set(opener, dialog);
package/dist/version.d.ts CHANGED
@@ -7,4 +7,4 @@
7
7
  * output stays what tsc emitted and nothing has to be substituted into it. A version bump
8
8
  * changes this line with package.json.
9
9
  */
10
- export declare const version = "v0.9.2";
10
+ export declare const version = "v0.9.4";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  * output stays what tsc emitted and nothing has to be substituted into it. A version bump
8
8
  * changes this line with package.json.
9
9
  */
10
- export const version = 'v0.9.2';
10
+ export const version = 'v0.9.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bkazemi/verily",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Self-hosted account-to-account verification for Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",