From e6cca0b20f8f9ddcd9f21abb83f2de10efa97f95 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 1 Oct 2026 01:49:06 +0200 Subject: [PATCH] Format 3, step 8: /create redesigned, and always light MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The author found the pages too technical and did not want a dark background. With the frontend design guidance: - The site is always light, whatever the system asks. - A capsule is a letter posted to the future: white paper, blue ink, an old-style serif for the voice of the page (Palatino, Iowan) and a DIN-like sans for the form (Bahnschrift), all system fonts, as the CSP allows no other origin. Blue is what you act on. - /create asks three questions, with short sentences: what it keeps, when it opens, who can open it. The date it opens is set large on an airmail envelope with the button that creates the capsule; quick dates of 1, 5 and 10 years; sizes in KB and MB; everything technical folded in "Detalles técnicos". The messages of the rules no longer name R4 or §29.6. Fixes of the review of the pages: - /inspect labels the start of a single file as content of the creator, unchecked, so that it cannot pass for a verdict; keeps ZWNJ and ZWJ in the comment and the author, which R4b allows; and says the right thing without files and without a comment. - /create pins at most 500 more rows with problems and counts the rest; shows what happened with a drop, not only to screen readers; a comment or an author over its limit is its own problem, not that of the files; a writing cannot start after the page is destroyed. Co-Authored-By: Claude Opus 5.5 --- src/app.css | 90 ++- src/app.html | 2 +- src/lib/components/OpenPanel.svelte | 15 +- src/lib/inspector/create-check.test.ts | 118 ++-- src/lib/inspector/create-check.ts | 89 ++- src/lib/inspector/create-input.test.ts | 12 +- src/lib/inspector/create-input.ts | 25 +- src/lib/inspector/format.test.ts | 9 + src/lib/inspector/format.ts | 8 +- src/routes/create/+page.svelte | 887 +++++++++++++------------ 10 files changed, 672 insertions(+), 583 deletions(-) diff --git a/src/app.css b/src/app.css index 1ff8711..27e69cc 100644 --- a/src/app.css +++ b/src/app.css @@ -1,22 +1,31 @@ -/* DateKeys inspector: the whole stylesheet. - - The palette is datekeys.com's: white paper, near-black ink and the brand - orange. Here colour always means something: green is a passed step, red a - rejection, orange a trust caveat (public data, informative views). Human - words are set in the system sans; values read from a capsule (hex, - identifiers, codes, CLI details) in the system monospace, never labels. - No web fonts: the Content-Security-Policy allows nothing but this origin. */ +/* DateKeys: the whole stylesheet. + A capsule is a letter posted to the future, and the pages are set like + one: white paper, blue ink, the voice of the page in an old-style serif + and the form in a DIN-like sans, as a postal form; the stripes of an + airmail envelope appear once, around the date a capsule opens. Colour + always means something: blue is what you act on, green a passed step, red + a rejection, orange a trust caveat. Values read from a capsule (hex, + identifiers, codes) are in the system monospace, never labels. Every face + is a system font: the Content-Security-Policy allows nothing but this + origin. The page is always light. */ :root { - color-scheme: light dark; + /* Always light, whatever the system asks: the author does not want a + dark page. */ + color-scheme: light; --paper: #ffffff; - --paper-2: #f4f4f3; - --ink: #0e0e0e; - --ink-muted: #545454; - --ink-faint: #6b6b6b; - --rule: #e2e2e0; - --rule-strong: #8f8f8c; + --paper-2: #f3f5f9; + --ink: #1c2b4b; + --ink-muted: #55627a; + --ink-faint: #6b7890; + --rule: #d8dde6; + --rule-strong: #7d889d; + /* The blue of what you act on, and the stripes of the airmail envelope. */ + --accent: #1f4fa3; + --accent-bg: #eef3fb; + --airmail-blue: #1f4fa3; + --airmail-red: #c8323c; --orange: #f18225; /* The logo orange is a fill; this deeper orange carries text, at least 4.5:1 on the paper and on the caution tint. */ @@ -27,41 +36,24 @@ --fail: #b3261e; --fail-bg: #fbecea; - --sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; + --sans: Bahnschrift, 'DIN Alternate', 'DIN 2014', 'Franklin Gothic Medium', 'Nimbus Sans Narrow', 'Segoe UI', system-ui, sans-serif; + --serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'URW Palladio L', P052, 'Book Antiqua', Georgia, serif; --mono: ui-monospace, 'Cascadia Mono', 'Segoe UI Mono', 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; /* Type ramp, x1.2 from 16 px. */ --t-small: 0.875rem; --t-body: 1rem; --t-lead: 1.2rem; - --t-h3: 1.2rem; - --t-h2: 1.44rem; - --t-date: clamp(1.6rem, 1.2rem + 1.6vw, 2.2rem); - --t-h1: clamp(2rem, 1.4rem + 2.4vw, 3rem); + --t-h3: 1.3rem; + --t-h2: 1.7rem; + --t-date: clamp(2rem, 1.3rem + 2.6vw, 3rem); + --t-h1: clamp(2.3rem, 1.5rem + 3vw, 3.4rem); --gut: 16px; --measure: 68ch; --radius: 4px; } -@media (prefers-color-scheme: dark) { - :root { - --paper: #0c0c0b; - --paper-2: #161615; - --ink: #f2f1ef; - --ink-muted: #b3b0ab; - --ink-faint: #9a9791; - --rule: #2a2a28; - --rule-strong: #6d6a66; - --orange-text: #f39a4e; - --caution-bg: #26190c; - --pass: #74d49c; - --pass-bg: #0f2418; - --fail: #ff8f85; - --fail-bg: #2c1311; - } -} - @media (min-width: 720px) { :root { --gut: 32px; @@ -99,13 +91,13 @@ h3, h4 { line-height: 1.2; margin: 0; - font-weight: 650; - letter-spacing: -0.01em; + font-family: var(--serif); + font-weight: 400; } h1 { font-size: var(--t-h1); - letter-spacing: -0.025em; + line-height: 1.08; } h2 { @@ -151,7 +143,7 @@ pre, } :focus-visible { - outline: 2px solid var(--ink); + outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; } @@ -221,17 +213,17 @@ button { gap: 0.5rem; min-height: 2.75rem; padding: 0.55rem 1.1rem; - border: 1px solid var(--ink); - border-radius: var(--radius); - background: var(--ink); + border: 1px solid var(--accent); + border-radius: 8px; + background: var(--accent); color: var(--paper); - font-weight: 600; + font-weight: 500; text-decoration: none; cursor: pointer; } .button:hover { - background: color-mix(in srgb, var(--ink) 86%, var(--paper)); + background: color-mix(in srgb, var(--accent) 82%, var(--ink)); } .button.quiet { @@ -241,8 +233,8 @@ button { } .button.quiet:hover { - border-color: var(--ink); - background: var(--paper-2); + border-color: var(--accent); + background: var(--accent-bg); } /* The step rail, shared by the landing and the report: the steps of §63 diff --git a/src/app.html b/src/app.html index cd840b7..634b56b 100644 --- a/src/app.html +++ b/src/app.html @@ -4,7 +4,7 @@ %sveltekit.head% - + diff --git a/src/lib/components/OpenPanel.svelte b/src/lib/components/OpenPanel.svelte index 277c2d7..9ebb4b7 100644 --- a/src/lib/components/OpenPanel.svelte +++ b/src/lib/components/OpenPanel.svelte @@ -548,16 +548,19 @@ {#if f.head.author !== ''}

autor declarado (texto del creador, sin comprobar):

-

{escapeInvisible(f.head.author)}

+

{escapeInvisible(f.head.author, true)}

{/if} {#if f.head.comment !== ''}
Comentario del creador (sin comprobar)
-
{escapeInvisible(f.head.comment)}
+
{escapeInvisible(f.head.comment, true)}
{/if} {#if v.shown} -
{v.shown.cut ? `${v.shown.text}\n…` : v.shown.text}
+
+
Principio de {f.facts[0]?.shown} (contenido del creador, sin comprobar)
+
{v.shown.cut ? `${v.shown.text}\n…` : v.shown.text}
+
{#if v.shown.cut}

Se muestra el principio del fichero, como mucho {formatInteger(SHOWN_TEXT)} caracteres.

{/if} @@ -584,14 +587,14 @@ Están en un fichero temporal privado de este navegador. Se borra cuando lo pides, al abrir o cargar otra cápsula y al salir de la página. {:else} - Están en la memoria de esta página, porque el navegador no le deja un fichero temporal privado. Se liberan - al abrir o cargar otra cápsula y al salir de la página. + Están en la memoria de esta página: el navegador no le deja un fichero temporal privado, o no tiene sitio + para ellos. Se liberan al abrir o cargar otra cápsula y al salir de la página. {/if}

{/if} {/if} {#if f.facts.length === 0} -

La cápsula no guarda ficheros, solo el comentario.

+

{f.head.comment === '' ? 'La cápsula no guarda ficheros ni comentario.' : 'La cápsula no guarda ficheros, solo el comentario.'}

{:else}

Ficheros ({formatInteger(f.facts.length)})

{#if shown.length < entries.length}

- La lista muestra {formatInteger(shown.length)} de {formatInteger(entries.length)} ficheros: los {formatInteger(LISTED)} primeros - y los que han tenido un problema. La cápsula guarda todos los marcados. + Se ven {formatInteger(shown.length)} de {formatInteger(entries.length)} ficheros: los {formatInteger(LISTED)} primeros y + los que han tenido algún problema. La cápsula guarda todos los marcados. +

+ {/if} + {#if unseen > 0} +

+ {unseen === 1 ? 'Hay 1 nombre más' : `Hay ${formatInteger(unseen)} nombres más`} con problemas que no caben en la lista. + Cámbialos en tu equipo y vuelve a añadir la carpeta.

{/if} {#if pathCheck?.overall !== undefined}

{pathCheck.overall}

{/if} +

+ {summary.files === 1 ? 'Un fichero' : `${formatInteger(summary.files)} ficheros`}, {size(summary.bytes)}{summary.left === 0 + ? '' + : `; ${summary.left === 1 ? 'uno queda fuera' : `${formatInteger(summary.left)} quedan fuera`}`}. +

{/if}

- Cada fichero se guarda con su ruta, que puedes cambiar: las carpetas elegidas ponen su nombre delante. Las rutas, las - fechas y el comentario van cifrados con los ficheros, y nadie los ve antes de la fecha. Las carpetas vacías no se - guardan, y los ficheros que crean los sistemas (.DS_Store, Thumbs.db, desktop.ini, - ._* y __MACOSX) quedan fuera, tachados, salvo que los marques. + Cada fichero conserva su nombre y su carpeta. Si un nombre no vale en Windows o en macOS, lo verás marcado y podrás + cambiarlo. Los ficheros que crea el sistema, como .DS_Store, quedan fuera salvo que los marques.

- + -

- {#if recipientCheck?.ok === false} - {recipientCheck.problem} - {:else if recipientCheck?.ok === true} - {recipientCheck.keys.length === 1 ? '1 destinatario.' : `${recipientCheck.keys.length} destinatarios.`} - {/if} - Opcional. Cada uno abrirá la cápsula con su identidad de age (AGE-SECRET-KEY-1…), que nunca se pega aquí. Como - mucho 15 con la clave portable, o 16 sin ella. -

-
- Qué es un destinatario de age y cómo se consigue -
    -
  1. - Quien vaya a abrir la cápsula crea su par de claves con la herramienta age: - age-keygen -o clave.txt. El fichero guarda su identidad secreta (AGE-SECRET-KEY-1…), - y el comando muestra su clave pública (age1…). -
  2. -
  3. Te manda solo la línea age1…, por el canal que sea: es pública.
  4. -
  5. - La pegas aquí, una por persona. Llegada la fecha, esa persona abre la cápsula en el - inspector con la firma de la ronda y su identidad secreta, que no sale de su - equipo. -
  6. -
-

- Solo valen claves X25519, las age1… de age-keygen: no valen las de SSH ni las - poscuánticas, que empiezan por age1pq1. Sin age, la clave portable (.dkk) hace - el mismo papel: la genera esta página y la envías tú. -

-
-
+
+ Dar la llave a personas con age +
+ + +

+ {#if recipientCheck?.ok === false} + {recipientCheck.problem} + {:else if recipientCheck?.ok === true} + {recipientCheck.keys.length === 1 ? '1 persona.' : `${recipientCheck.keys.length} personas.`} + {/if} + Cada una abrirá la cápsula con su clave secreta de age, que nunca se pega aquí. Como mucho 15 con la + llave, o 16 sin ella. Quien no tenga age la crea con age-keygen y te manda solo la línea + age1…. +

+
+
{/if} -
-

4. Antes de cifrar

+
+

Antes de crear

+ {#if plan} + {@const when = envelopeDate(plan.effectiveMs, timeZone)} +

Se abrirá el

+

{when.day}

+

a las {when.hour}{plan.soon ? `, ${formatRelative(plan.effectiveMs, nowMs)}` : ''}

+

{contents(plan)}. La cápsula ocupará {size(plan.size)}.

+ {:else} +

Todavía falta algo

+

{planned.ok ? '' : planned.problem}

+ {/if} +
+ + {#if busy} + + {/if} +
+ {#if busy} +
+ +

+ {pass === 1 ? 'Leyendo los ficheros' : 'Cifrando y guardando'}: {percent(written, total)}, {size(written)} de {size(total)}. +

+
+ {/if} +
+ + {#if problem} +

{problem}

+ {/if} + +
    +
  • DateKeys {SPEC_VERSION} es una versión de prueba: una versión futura podría no abrir esta cápsula.
  • + {#if plan?.longHorizon} +
  • + Falta más de un año. Para abrirla hará falta la firma que la red drand publique ese día, y si nadie la conserva, la + cápsula no se abrirá. Su cifrado tampoco resiste un ordenador cuántico futuro. +
  • + {/if} +
+ +
+ Detalles técnicos {#if plan}
-
Se abrirá
+
Instante de apertura
- {formatDateTime(plan.effectiveMs, timeZone)} - ({formatDateTime(plan.effectiveMs, UTC)}) + {formatDateTime(plan.effectiveMs, UTC)}, el de la ronda {plan.dateKey.round} de Quicknet, la red de drand. {#if plan.effectiveMs !== plan.requestedMs} -
- Es la hora de la ronda: {seconds(plan.effectiveMs - plan.requestedMs)} s después de la pedida, porque drand - firma una ronda cada 3 s (§15). - + Cae {seconds(plan.effectiveMs - plan.requestedMs)} s después de la hora pedida, porque drand firma una ronda cada 3 s + (§15). {/if} {#if plan.ambiguous} -
Esa hora se repite ese día en {timeZone}, al atrasar los relojes: vale la segunda. + Esa hora se repite ese día en {timeZone}, al atrasar los relojes: vale la segunda. {/if}
-
-
Ronda
-
{plan.dateKey.round} de Quicknet, la red de drand
-
DateKey
{plan.dk1}
-
Ahora, en este dispositivo
+
Reloj de este dispositivo
- {formatDateTime(nowMs, timeZone)} ({formatDateTime(nowMs, UTC)}) -
- Si este reloj va atrasado, la cápsula podría sellarse para una ronda ya publicada, que cualquiera con el - .dkc abriría enseguida. La página no pregunta la hora a ningún servidor. - + {formatDateTime(nowMs, UTC)}. Si va atrasado, la cápsula podría sellarse para una ronda ya publicada, y se abriría + enseguida: la página no pregunta la hora a ningún servidor.
-
Contenido
-
{contents(plan)}
-
-
-
Tamaño del .dkc
-
{formatByteCount(plan.size)}
+
Tamaño
+
+ {formatByteCount(plan.size)}: formato 3, con el contenido, sus nombres y sus fechas ({formatByteCount(plan.length)}) + rellenados con ceros hasta {formatByteCount(plan.paddedLength)} (relleno reforzado, §29.1). +
-
Qué deja ver hasta la fecha
+
Qué se ve antes de la fecha
- La fecha de apertura, la política, el capsule_id y ese tamaño, que da el del contenido de forma - aproximada: los ficheros, sus rutas y el comentario, con un margen de 256 bytes hasta 8 KiB, y de menos de un 6,25 % - por encima. No deja ver el tamaño exacto, cuántos ficheros guarda ni cuántas credenciales la abren, que no es lo - mismo que cuántas personas; esto último se apoya en Diffie–Hellman, que no resiste un adversario cuántico futuro, - como el resto del cifrado (§55.2, §53). + La fecha de apertura, el tipo de cápsula, su capsule_id y ese tamaño, que da el del contenido de forma + aproximada. No se ve cuántos ficheros guarda, salvo un máximo que se deduce de ese tamaño, ni cuántas llaves la abren + (§55.2, §53).
- {:else if !planned.ok} -

{planned.problem}

{/if} - -
    -
  • - Protocolo preliminar: DateKeys {SPEC_VERSION} todavía no es la versión 1.0, y hasta entonces pueden cambiar los esquemas - de la cápsula (§74), así que una versión futura podría no abrir esta cápsula. -
  • - {#if plan?.longHorizon} -
  • - Aviso: el cifrado por tiempo de Quicknet V1 no es poscuántico. El texto cifrado puede seguir guardado durante años, y - su confidencialidad futura depende del proveedor y de la criptografía en que se basa. -
  • -
  • - Para abrirla hará falta el release de su ronda, que publica la red drand. Si en esa fecha ningún relay ni ninguna copia - conservada lo ofrece, la cápsula no podrá abrirse. -
  • - {/if} - {#if plan?.soon} -
  • - La cápsula se podrá abrir {formatRelative(plan.effectiveMs, nowMs)}: desde entonces la abrirá quien tenga el - .dkc{plan.policy === TIME_AND_KEY ? ' y una de sus credenciales' : ''}. -
  • - {/if} -
-
- - {#if problem} -

{problem}

- {/if} - -
- - {#if busy} - - {/if} -
- {#if busy} -
- -

- {pass === 1 ? 'Paso 1 de 2, leer los ficheros para su SHA-256' : 'Paso 2 de 2, cifrar los ficheros y escribir el .dkc'}: - {percent(written, total)}, {formatByteCount(written)} de {formatByteCount(total)}. -

-
- {:else}

- Los ficheros se leen dos veces: primero para calcular el SHA-256 de cada uno, y después para cifrarlos. Si uno cambia - entre las dos lecturas, no se crea la cápsula. El .dkc se escribe en un fichero temporal privado de este - navegador, que solo se da por bueno cuando la cápsula está completa y comprobada. Se borra cuando lo pides, al crear otra y - al salir de la página; sin ese fichero, se escribe en la memoria de la página hasta 64 MiB. Todo corre en esta pestaña: - con ficheros grandes, la página puede ir lenta mientras cifra. + Los ficheros se leen dos veces: primero para calcular el SHA-256 de cada uno, y después para cifrarlos. Si uno cambia entre + las dos lecturas, no se crea la cápsula. El .dkc se escribe en un fichero temporal privado de este navegador, + que solo se da por bueno cuando la cápsula está completa y comprobada, y se borra al crear otra o al salir de la página. + Sin ese fichero, se escribe en la memoria de la página, hasta 64 MiB.

- {/if} +

{announcement}

{#if result} {@const r = result} + {@const when = envelopeDate(r.plan.effectiveMs, timeZone)}
-
-

Cápsula creada

-

- Se abrirá el {formatDateTime(r.plan.effectiveMs, timeZone)} ({formatDateTime(r.plan.effectiveMs, UTC)}), con la - ronda {r.plan.dateKey.round}. Cifrada y comprobada en {seconds(r.ms)} s. A partir de esa fecha se abre en el - inspector o con datekeys decrypt. +

+

Cápsula cerrada. Se abrirá el

+

{when.day}

+

a las {when.hour}

+

+ {contents(r.plan)}. Desde esa fecha se abre en el inspector o con + datekeys decrypt.

-
-
-
capsule_id
-
{r.capsuleId}
-
-
-
Política
-
{r.plan.policy === TIME_AND_KEY ? `fecha y clave, con ${credentials(r.plan)}` : 'solo fecha'}
-
-
-
Contenido
-
{contents(r.plan)}
-
-
-
Tamaño
-
- {formatByteCount(r.capsule.size)}: formato 3, con el contenido, sus rutas y sus fechas ({formatByteCount(r.plan.length)}) - rellenados con ceros hasta {formatByteCount(r.plan.paddedLength)} (relleno reforzado, §29.1) -
-
-
-

La cápsula (.dkc)

+

La cápsula

{#if capsuleGone}

{r.temporary @@ -1122,13 +1105,10 @@

-

- El nombre se ve junto al fichero: el propuesto solo repite la fecha de apertura, que la cápsula ya muestra, y no - dice cuándo se creó. -

+

El nombre propuesto solo repite la fecha de apertura, que la cápsula ya muestra.

- + {#if r.temporary} {/if} @@ -1145,12 +1125,12 @@ {#if r.plan.portable}
-

La clave portable (.dkk)

+

La llave

{#if hasKey}

- Guarda esta clave en secreto: quien la tenga podrá abrir la cápsula desde la fecha. Solo sirve para esta cápsula. + Guárdala en secreto: quien la tenga podrá abrir la cápsula desde la fecha. Solo sirve para esta cápsula. {#if r.keyOnly} - Es la única credencial: sin ella, nadie podrá abrir la cápsula, tampoco tú. + Es la única llave: sin ella nadie podrá abrir la cápsula, tampoco tú. {/if}

@@ -1158,22 +1138,39 @@
- - + +

- Está solo en la memoria de esta página, nunca en el almacenamiento del navegador. Se borra cuando pulsas «Olvidar la - clave», al crear otra cápsula y al salir de la página. Suele convenir enviarla por otro camino que el .dkc. + Está solo en la memoria de esta página. Se borra al pulsar «Olvidar la llave», al crear otra cápsula y al salir de la + página. Conviene enviarla por otro camino que la cápsula.

{:else}

- La clave ya no está en la página.{keySaved ? '' : ' No se descargó: si no hay otra credencial, esta cápsula no podrá abrirse.'} + La llave ya no está en la página.{keySaved ? '' : ' No se descargó: si no hay otra, esta cápsula no podrá abrirse.'}

{/if}
{/if} - +
+ Informe técnico de la cápsula +
+
+
capsule_id
+
{r.capsuleId}
+
+
+
Tipo
+
{r.plan.policy === TIME_AND_KEY ? `con llave: ${credentials(r.plan)}` : 'solo fecha'}
+
+
+
Tamaño
+
{formatByteCount(r.capsule.size)}. Cifrada y comprobada en {seconds(r.ms)} s.
+
+
+ +
{/if}
@@ -1190,81 +1187,67 @@ .page { display: grid; grid-template-columns: minmax(0, 1fr); - gap: 2rem; - padding-top: clamp(2rem, 5vw, 3.5rem); + gap: 2.5rem; + max-width: 44rem; + padding-top: clamp(2.5rem, 6vw, 4.5rem); + padding-bottom: 4rem; } - .intro .lead { - margin-top: 1rem; + .intro { + display: grid; + gap: 0.9rem; } .form { display: grid; grid-template-columns: minmax(0, 1fr); - gap: 1.75rem; - max-width: var(--measure); + gap: 3rem; } .step { display: grid; grid-template-columns: minmax(0, 1fr); - gap: 0.75rem; + gap: 1rem; min-width: 0; - } - .step h2, - .legend { - font-size: var(--t-h3); - font-weight: 650; - } - fieldset { margin: 0; padding: 0; border: 0; } + .step h2, + .legend { + font-family: var(--serif); + font-size: var(--t-h2); + font-weight: 400; + color: var(--ink); + } legend { padding: 0; - margin-bottom: 0.75rem; + margin-bottom: 1rem; } .drop { display: grid; - place-items: center; - align-content: center; - gap: 0.35rem; - min-height: 10rem; - padding: 1.5rem; - border: 2px dashed var(--rule-strong); - border-radius: var(--radius); + gap: 0.2rem; + padding: 1.6rem 1.25rem; + border: 1.5px dashed var(--rule-strong); + border-radius: 10px; background: var(--paper-2); text-align: center; cursor: pointer; - overflow-wrap: anywhere; } .drop:hover, .drop.dragging { - border-color: var(--ink); + border-color: var(--accent); + background: var(--accent-bg); } .drop:has(input:focus-visible) { - outline: 2px solid var(--ink); + outline: 2px solid var(--accent); outline-offset: 3px; - border-color: var(--ink); } .drop:has(input[aria-invalid='true']) { border-color: var(--fail); } - .drop-icon { - width: 3rem; - height: 3rem; - margin-bottom: 0.5rem; - fill: none; - stroke: var(--ink); - stroke-width: 2; - } - .drop-icon .seal { - fill: var(--orange); - stroke: none; - } .drop-title { - font-size: var(--t-h3); - font-weight: 650; + font-size: var(--t-lead); + color: var(--accent); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.2em; @@ -1273,73 +1256,97 @@ color: var(--ink-muted); } - .list-summary { - font-weight: 600; - } .files { display: grid; - gap: 0.6rem; margin: 0; padding: 0; list-style: none; + border-top: 1px solid var(--rule); } .files li { display: grid; - grid-template-columns: auto minmax(0, 1fr); - gap: 0.6rem; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 0.75rem; align-items: start; + padding: 0.55rem 0; + border-bottom: 1px solid var(--rule); } .files li > input[type='checkbox'] { - margin-top: 0.7rem; + margin-top: 0.55rem; } .entry { display: grid; - gap: 0.2rem; + gap: 0.15rem; min-width: 0; } .entry .path { - font-family: var(--mono); - font-size: 0.85rem; + width: 100%; + padding: 0.3rem 0.45rem; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--ink); + font: inherit; + line-height: 1.4; + overflow-wrap: anywhere; field-sizing: content; resize: none; } - .files .left .path { + .entry .path:hover:not(:disabled) { + border-color: var(--rule); + } + .entry .path:focus { + border-color: var(--accent); + background: var(--paper); + } + .wrong .path { + border-color: var(--fail); + background: var(--fail-bg); + } + .left .path { text-decoration: line-through; - color: var(--ink-muted); + color: var(--ink-faint); } .facts { + display: grid; + padding-inline: 0.45rem; font-size: var(--t-small); color: var(--ink-muted); - overflow-wrap: anywhere; } - .facts .note { - display: block; + .facts:empty { + display: none; } - textarea.prose { - font-family: inherit; - font-size: inherit; + .size { + padding-top: 0.35rem; + font-size: var(--t-small); + color: var(--ink-muted); + font-variant-numeric: tabular-nums; + white-space: nowrap; } - - .when { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 0.9rem; + .list-summary { + color: var(--ink-muted); } - @media (min-width: 560px) { - .when { - grid-template-columns: minmax(0, 1fr) minmax(0, 0.7fr); - } - .when .zone { - grid-column: 1 / -1; - } + .notice { + padding: 0.6rem 0.85rem; + border-radius: 8px; + background: var(--caution-bg); + font-size: var(--t-small); } + .field { display: grid; gap: 0.35rem; min-width: 0; } + .field.narrow { + max-width: 24rem; + } label { - font-weight: 600; + font-weight: 500; + } + .optional { + font-weight: 400; + color: var(--ink-muted); } input[type='date'], input[type='time'], @@ -1349,137 +1356,206 @@ width: 100%; min-width: 0; font: inherit; - padding: 0.45rem 0.6rem; + padding: 0.55rem 0.7rem; border: 1px solid var(--rule-strong); - border-radius: var(--radius); + border-radius: 8px; background: var(--paper); color: var(--ink); } textarea { - font-family: var(--mono); - font-size: 0.82rem; - line-height: 1.45; resize: vertical; overflow-wrap: anywhere; } + #recipients-input { + font-family: var(--mono); + font-size: 0.85rem; + } + textarea.letter { + font-family: var(--serif); + font-size: 1.15rem; + line-height: 1.6; + padding: 0.8rem 0.95rem; + } + input:focus-visible, + select:focus-visible, + textarea:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; + } [aria-invalid='true'] { border-color: var(--fail); } + .quick { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + .when { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 0.9rem; + } + @media (min-width: 560px) { + .when { + grid-template-columns: minmax(0, 1fr) minmax(0, 0.7fr); + } + .when .zone { + grid-column: 1 / -1; + } + } + .choice, .check { display: grid; grid-template-columns: auto minmax(0, 1fr); - gap: 0.6rem; + gap: 0.7rem; align-items: start; - padding: 0.75rem 0.9rem; - border: 1px solid var(--rule); - border-radius: var(--radius); font-weight: 400; cursor: pointer; } + .choice { + padding: 0.9rem 1rem; + border: 1px solid var(--rule); + border-radius: 10px; + } .choice + .choice { margin-top: 0.6rem; } .choice:has(input:checked) { - border-color: var(--ink); - background: var(--paper-2); + border-color: var(--accent); + background: var(--accent-bg); } .choice input, .check input { margin-top: 0.3rem; + accent-color: var(--accent); } - .choice strong { + .choice-title { display: block; + font-weight: 500; } .choice .hint { display: block; } .keyed { display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 0.9rem; + gap: 1rem; margin-top: 1rem; } - .check { - border: 0; - padding: 0; - } .hint { font-size: var(--t-small); color: var(--ink-muted); - max-width: var(--measure); - } - .help { - font-size: var(--t-small); - color: var(--ink-muted); - max-width: var(--measure); } - .help summary { + .help summary, + .tech summary { width: fit-content; - font-weight: 600; - color: var(--ink); cursor: pointer; + color: var(--accent); } - .help ol { - margin: 0.5rem 0; - padding-left: 1.25rem; - display: grid; - gap: 0.35rem; - } - .help code { - overflow-wrap: anywhere; + .help[open] summary { + margin-bottom: 0.75rem; } .inline-problem { display: block; color: var(--fail); - font-weight: 600; } - .summary dl, - .verdict dl { + /* The envelope: the one loud thing on the page. It holds the date the + capsule opens and the button that seals it, framed by the stripes of an + airmail envelope, a letter posted to the future. */ + .envelope { display: grid; - border-top: 1px solid var(--rule); - } - dl > div { - display: grid; - gap: 0.1rem 1rem; - padding-block: 0.55rem; - border-bottom: 1px solid var(--rule); + gap: 0.25rem; + padding: 1.6rem clamp(1.1rem, 4vw, 2rem) 1.7rem; + border: 9px solid transparent; + border-image: repeating-linear-gradient( + -45deg, + var(--airmail-red) 0 13px, + var(--paper) 13px 20px, + var(--airmail-blue) 20px 33px, + var(--paper) 33px 40px + ) + 9; + background: var(--paper); } - @media (min-width: 560px) { - dl > div { - grid-template-columns: 11rem minmax(0, 1fr); - } + .envelope.pending { + border-image: none; + border: 1px dashed var(--rule-strong); + border-radius: 10px; } - dt { - font-size: var(--t-small); - font-weight: 600; + .env-label { + font-size: var(--t-body); + font-weight: 400; color: var(--ink-muted); } - dd { - margin: 0; - overflow-wrap: anywhere; + .env-date { + font-family: var(--serif); + font-size: var(--t-date); + line-height: 1.1; + color: var(--ink); } - dd.mono { - font-size: 0.85rem; + .env-hour { + font-family: var(--serif); + font-size: var(--t-lead); + color: var(--ink); + } + .env-contents, + .env-missing { + margin-top: 0.6rem; + color: var(--ink-muted); + } + .envelope .actions { + margin-top: 1.2rem; } .notices { display: grid; - gap: 0.5rem; + gap: 0.4rem; margin: 0; padding: 0; list-style: none; + font-size: var(--t-small); + color: var(--ink-muted); } - .notices li, .caution { - padding: 0.6rem 0.85rem; - border-left: 4px solid var(--orange); - border-radius: 0 var(--radius) var(--radius) 0; + padding: 0.7rem 0.9rem; + border-radius: 8px; background: var(--caution-bg); + color: var(--ink); + } + + .tech { + display: grid; + gap: 1rem; font-size: var(--t-small); + color: var(--ink-muted); + } + .tech[open] summary { + margin-bottom: 1rem; + } + .tech dl { + display: grid; + border-top: 1px solid var(--rule); + } + .tech dl > div { + display: grid; + gap: 0.1rem 1rem; + padding-block: 0.55rem; + border-bottom: 1px solid var(--rule); + } + @media (min-width: 560px) { + .tech dl > div { + grid-template-columns: 11rem minmax(0, 1fr); + } + } + dt { + color: var(--ink); + } + dd { + margin: 0; + overflow-wrap: anywhere; } .actions { @@ -1495,47 +1571,38 @@ .progress { display: grid; gap: 0.35rem; + margin-top: 0.9rem; } progress { width: 100%; - height: 0.9rem; - accent-color: var(--orange); + height: 0.6rem; + accent-color: var(--accent); } .problem { - border-left: 4px solid var(--fail); + padding: 0.75rem 1rem; + border-radius: 8px; background: var(--fail-bg); - padding: 0.65rem 0.9rem; - border-radius: 0 var(--radius) var(--radius) 0; + color: var(--fail); overflow-wrap: anywhere; } .created { display: grid; grid-template-columns: minmax(0, 1fr); - gap: 2rem; + gap: 2.25rem; } - .verdict { - display: grid; - gap: 0.75rem; - max-width: var(--measure); - padding: 1.1rem 1.25rem 1.25rem; - border: 1px solid var(--rule); - border-top: 6px solid var(--pass); - border-radius: var(--radius); - background: var(--paper-2); - } - .state-word { - font-size: var(--t-h2); - font-weight: 700; - letter-spacing: -0.015em; - line-height: 1.2; - color: var(--pass); + .envelope.sealed .env-label { + color: var(--ink); } .block { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.9rem; - max-width: var(--measure); + } + .block h3 { + font-family: var(--serif); + font-size: var(--t-h3); + font-weight: 400; } .drop-overlay { @@ -1546,18 +1613,18 @@ place-items: center; padding: 1rem; pointer-events: none; - background: color-mix(in srgb, var(--paper) 88%, transparent); + background: color-mix(in srgb, var(--paper) 90%, transparent); } .drop-overlay::before { content: ''; position: absolute; inset: 12px; - border: 3px dashed var(--ink); - border-radius: 8px; + border: 3px dashed var(--accent); + border-radius: 12px; } .drop-overlay p { + font-family: var(--serif); font-size: var(--t-h2); - font-weight: 700; text-align: center; }