Files
VoxVera/site/nostr-client.css
T
thePR0M3TH3ANandClaude Opus 4.8 18fa382632 Add NIP-46 nostrconnect (QR) login path; move board safety link to footer
NIP-46 now supports both connect directions on the editor and the board:
- bunker:// (existing): paste a link the signer app generated.
- nostrconnect:// (new): the client generates the link, renders it as a
  QR (+ a copyable link for one-device use), listens on a rendezvous
  relay (relay.nsec.app), and the signer app scans it and connects back —
  proven by echoing the client's random secret. The discovered signer
  pubkey then drives the durable connection (shared adopt path).

Both paths stay session-only with an ephemeral local key; the QR uses
the same hand-rolled NIP-44 client (NIP-04 decrypt fallback). qrcode-
generator is now loaded on the board too. 3 new localized strings
(scan / waiting / copy-link) across all 14 languages in NOSTR_UI and
BOARD_UI; i18n parity passes.

Also per feedback: the board's "Safety & privacy" link moves out of the
topbar (where it ran together with "Back to VoxVera") into a centered
footer link matching the home page, and both pages now link /safety.html
(works on the static host and Vercel alike).

Tests: editor and board each connect via a fake signer that "scans" the
nostrconnect QR (reads the on-screen URI, echoes the secret) — all 6
engines. Full suite 372 passing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 21:49:37 -04:00

1741 lines
30 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:root {
color-scheme: light;
--ink: #111;
--muted: #5b5b5b;
--line: #c9c9c9;
--soft: #f3f3f1;
--accent: #b80000;
--ok: #1d6b3a;
--warn: #8a4d00;
/* Printable sheet dimensions. Switched by the paper-* class on <html>
(applyPaperSize in nostr-client.js). The tear-off column (3.75in) and QR
codes (1.25in) stay fixed physical sizes; the content column flexes. */
--sheet-w: 8.5in;
--sheet-h: 11in;
}
html.paper-letter {
--sheet-w: 8.5in;
--sheet-h: 11in;
}
html.paper-a4 {
--sheet-w: 210mm;
--sheet-h: 297mm;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
background: #fff;
font-family: "Courier New", Courier, monospace;
line-height: 1.45;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
min-height: 40px;
padding: 8px 14px;
border: 1px solid #000;
background: #000;
color: #fff;
cursor: pointer;
}
button:hover {
background: #333;
}
.secondary-button {
background: #fff;
color: #000;
}
.secondary-button:hover {
background: #eee;
}
.topbar {
display: flex;
gap: 24px;
justify-content: space-between;
align-items: flex-end;
padding: 20px clamp(16px, 4vw, 42px);
border-bottom: 2px solid #000;
}
.topbar h1,
.topbar p,
.section-title h2,
.preview-toolbar h2 {
margin: 0;
}
.topbar h1 {
font-size: 24px;
}
.topbar p {
color: var(--muted);
}
.mode-tabs {
display: inline-flex;
border: 1px solid #000;
}
.topbar-controls {
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
}
.topbar-controls select {
width: auto;
min-height: 40px;
max-width: 220px;
}
/* Wider so the full paper size (e.g. "US Letter (8.5 × 11 in)") is readable. */
#topbar-paper,
#viewer-paper {
min-width: 210px;
max-width: 260px;
}
.tab {
border: 0;
border-right: 1px solid #000;
background: #fff;
color: #000;
}
.tab:last-child {
border-right: 0;
}
.tab.active {
background: #000;
color: #fff;
}
main {
padding: 18px clamp(16px, 4vw, 42px) 36px;
}
.panel {
display: none;
}
.panel.active {
display: block;
}
.viewer-floating {
display: none;
}
.workspace {
display: grid;
grid-template-columns: minmax(320px, 1.15fr) minmax(280px, 0.85fr);
gap: 24px;
align-items: start;
}
.editor-form,
.result-panel {
min-width: 0;
}
.section-title,
.preview-toolbar {
display: flex;
gap: 16px;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.state-pill {
display: inline-block;
padding: 4px 8px;
border: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
}
.identity-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin: 0 0 12px;
}
.identity-row .secondary-button {
min-height: 32px;
padding: 5px 10px;
font-size: 12px;
}
/* Identity mode controls (anonymous / NIP-07 / imported nsec). */
.identity-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 10px;
}
.identity-controls .secondary-button {
min-height: 32px;
padding: 5px 10px;
font-size: 12px;
}
/* Vertical stack so the input, the "remember" checkbox, the PIN field, and the
action button each get their own line (a wrapping row collided the label and
button). */
/* The `hidden` attribute must win over the display rule below (otherwise the
nsec-import box and the locked-identity row show on every load). */
.identity-import[hidden] {
display: none;
}
.identity-import {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
margin: 0 0 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--soft);
}
.identity-import input[type="password"],
.identity-import input[type="text"] {
width: 100%;
box-sizing: border-box;
font: inherit;
padding: 6px 8px;
border: 1px solid var(--line);
border-radius: 6px;
}
.identity-import .secondary-button {
align-self: flex-start;
min-height: 32px;
padding: 5px 12px;
font-size: 12px;
}
.identity-import .nsec-remember-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--muted);
}
.identity-import .nsec-remember-row input {
width: auto;
flex: 0 0 auto;
margin: 0;
}
#nip46-qr-wrap[hidden],
#board-nip46-qr-wrap[hidden] {
display: none;
}
#nip46-qr-wrap,
#board-nip46-qr-wrap {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
margin-top: 8px;
}
.nip46-qr {
width: 200px;
max-width: 100%;
align-self: center;
background: #fff;
padding: 8px;
box-sizing: border-box;
border: 1px solid var(--line);
border-radius: 6px;
}
.nip46-qr svg {
display: block;
width: 100%;
height: auto;
}
#nip46-uri,
#board-nip46-uri {
font-family: "Courier New", monospace;
font-size: 11px;
}
/* The locked row keeps the note on its own line, then PIN + buttons below. */
#identity-locked-row {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
#identity-locked-note {
font-size: 12px;
color: var(--muted);
flex: 1 1 100%;
}
#identity-locked-row input[type="password"] {
width: auto;
flex: 1 1 160px;
}
.editor-form label {
display: grid;
gap: 5px;
margin-bottom: 12px;
font-size: 13px;
font-weight: 700;
}
input,
select,
textarea {
width: 100%;
border: 1px solid #000;
border-radius: 0;
background: #fff;
color: #000;
padding: 8px;
}
textarea {
resize: vertical;
}
.field-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px;
gap: 12px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.result-list {
display: grid;
grid-template-columns: 90px minmax(0, 1fr);
gap: 8px 12px;
margin: 0 0 12px;
}
.result-list dt {
font-weight: 700;
}
.result-list dd {
margin: 0;
min-width: 0;
}
.copy-value {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: start;
}
.copy-button {
min-height: 28px;
padding: 4px 8px;
font-size: 11px;
}
.relay-results {
display: grid;
gap: 4px;
}
.relay-ok {
color: var(--ok);
font-weight: 700;
}
.relay-error {
color: var(--warn);
}
code {
display: block;
overflow-wrap: anywhere;
white-space: normal;
}
.status-line {
min-height: 22px;
color: var(--muted);
}
.field-error {
margin: 0;
color: var(--warn);
font-size: 12px;
font-weight: 700;
}
.preview-band {
margin-top: 26px;
border-top: 2px solid #000;
padding-top: 18px;
}
.flyer-preview {
overflow-x: auto;
padding-bottom: 8px;
}
.flyer-sheet {
display: flex;
width: var(--sheet-w);
height: var(--sheet-h);
border: 1px solid #000;
background: #fff;
color: #000;
overflow: hidden;
}
.flyer-sheet.no-tear-offs .tear-offs {
display: none;
}
.flyer-sheet.no-tear-offs .flyer-content {
width: 100%;
}
.tear-offs {
width: 3.75in;
padding: 8px;
border-right: 1px dashed #000;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
}
.tear-off {
display: flex;
gap: 12px;
height: 1in;
padding: 5px;
border: 1px dashed #000;
font-size: 10px;
overflow: hidden;
}
.qr-box {
flex: 0 0 0.72in;
width: 0.72in;
height: 0.72in;
border: 1px solid #000;
display: grid;
place-items: center;
font-size: 9px;
text-align: center;
}
.flyer-content {
width: 4.75in;
padding: 0.28in;
display: flex;
flex-direction: column;
justify-content: flex-start;
text-align: center;
}
.flyer-content h1 {
margin: 0 0 0.18in;
font-size: 34px;
line-height: 1.05;
}
.flyer-content .subtitle {
margin-bottom: 0.16in;
color: var(--accent);
font-weight: 700;
font-size: 21px;
}
.message {
margin-top: 0.12in;
text-align: left;
font-size: 15px;
white-space: pre-wrap;
}
.main-qr {
align-self: center;
margin-top: 0.18in;
width: 1.25in;
height: 1.25in;
}
.footer {
margin-top: auto;
font-size: 11px;
overflow-wrap: anywhere;
}
a {
color: var(--accent);
}
.container {
display: flex;
flex-direction: row;
width: var(--sheet-w);
height: var(--sheet-h);
border: 1px solid #000;
background: #fff;
color: #000;
box-sizing: border-box;
overflow: hidden;
page-break-inside: avoid;
/* Per-script letter-spacing, set by the ls-* policy class (see
LETTER_SPACING_POLICY in nostr-client.js). Defaults below match the
historical design and act as a safe fallback if no policy class is set. */
--ls-display: 2px;
--ls-subtitle: 6px;
--ls-body: 0;
--ls-url: 0;
--ls-footer: 0;
}
/* Latin, Cyrillic, Hebrew: tighten tracking to reclaim line space. */
.container.ls-full {
--ls-display: 0.5px;
--ls-subtitle: 2px;
--ls-body: -0.3px;
--ls-url: -0.3px;
--ls-footer: 0;
}
/* CJK: tracking is safe but must never go negative (glyphs would overlap). */
.container.ls-cjk {
--ls-display: 0;
--ls-subtitle: 0;
--ls-body: 0;
--ls-url: 0;
--ls-footer: 0;
}
/* Arabic, Persian, Hindi: letter-spacing breaks shaping — turn it off. */
.container.ls-none {
--ls-display: normal;
--ls-subtitle: normal;
--ls-body: normal;
--ls-url: normal;
--ls-footer: normal;
}
.container.no-tear-offs .left-tear-offs {
display: none;
}
.container.no-tear-offs .content {
width: 100%;
}
.left-tear-offs {
width: 3.75in;
padding: 8px;
border-right: 1px dashed #000;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
box-sizing: border-box;
}
.tear-off-text {
flex: 1;
min-width: 0;
padding-top: 1px;
overflow-wrap: anywhere;
word-break: normal;
hyphens: none;
display: flex;
flex-direction: column;
}
/* Tear-off title labels each tab. Its font-size is set by fitTabTitles() in JS
to be as large as fits; this caps the height so a long title shrinks (and at
worst clips) instead of pushing the link/event-id out of the 1in tab. */
.tear-off-title {
font-weight: 700;
line-height: 1.05;
text-align: center;
letter-spacing: 0;
max-height: 0.42in;
overflow: hidden;
margin-bottom: 2px;
}
/* The clickable action line under the title (also clickable in a printed PDF). */
.tear-off-link {
display: block;
font-size: 10px;
line-height: 1.15;
text-align: center;
}
.tear-off-event-id {
margin-top: auto;
padding-top: 4px;
font-size: 8px;
line-height: 1.2;
word-break: break-all;
overflow-wrap: anywhere;
}
.tear-off-event-id-value {
display: block;
font-family: "Courier New", monospace;
font-size: 12px;
line-height: 1.1;
letter-spacing: -0.2px;
word-break: break-all;
}
.qr-code {
width: 80px;
height: 80px;
flex-shrink: 0;
}
.qr-code svg {
display: block;
width: 100%;
height: 100%;
}
.qr-error {
display: grid;
width: 100%;
height: 100%;
place-items: center;
border: 1px solid #000;
font-size: 9px;
text-align: center;
}
.qr-placeholder {
width: 80px;
height: 80px;
flex-shrink: 0;
background:
linear-gradient(90deg, #000 8px, transparent 8px 16px, #000 16px 24px, transparent 24px 32px),
linear-gradient(#000 8px, transparent 8px 16px, #000 16px 24px, transparent 24px 32px);
background-color: #fff;
background-size: 32px 32px;
border: 8px solid #000;
}
.content {
width: calc(var(--sheet-w) - 3.75in);
padding: 15px 20px;
text-align: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
page-break-inside: avoid;
overflow-wrap: break-word;
}
.flyer-status-message {
margin: auto;
padding: 0 10px;
font-size: 20px;
font-weight: 700;
line-height: 1.3;
text-align: center;
letter-spacing: var(--ls-display);
}
.content h1 {
margin: 0;
padding: 0;
font-size: 24px;
line-height: 1.2;
letter-spacing: var(--ls-display);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.distribute {
font-size: 18px;
line-height: 1.2;
letter-spacing: var(--ls-subtitle);
margin: 2px 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.container .message {
flex: 1;
margin-top: 4px;
font-size: 14px;
line-height: 1.4;
letter-spacing: var(--ls-body);
text-align: left;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
white-space: pre-wrap;
box-sizing: border-box;
position: relative;
}
.qr-code-body {
width: 100%;
height: auto;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 0;
box-sizing: border-box;
}
.qr-code-body .main-qr {
width: 80px;
height: 80px;
}
.qr-code-url {
font-size: 12px;
letter-spacing: var(--ls-url);
text-align: left;
overflow-wrap: anywhere;
word-break: normal;
hyphens: none;
flex: 1 1 auto;
min-width: 0;
padding-left: 4px;
}
hr {
border: none;
border-top: 1px solid #000;
width: 100%;
margin: 4px 0;
}
.container .footer {
font-size: 10px;
overflow-wrap: break-word;
text-align: center;
align-self: center;
width: 100%;
padding: 10px 0;
box-sizing: border-box;
border-top: 1px solid #000;
}
.container .footer p {
margin: 0;
padding: 0;
}
.container .footer .binary {
margin-bottom: 8px;
margin-top: 10px;
letter-spacing: var(--ls-footer);
}
.container .footer .credit {
font-size: 12px;
margin-top: 0;
}
/* Screen-only link to the bulletin board, shown below the flyer preview
(outside the printed sheet). Hidden in print (see @media print). */
.preview-band > .board-link {
text-align: center;
font-size: 13px;
margin: 12px 0;
}
.board-link a {
color: var(--accent);
}
/* Footer "Safety & privacy" link on the board page (mirrors the home page). */
.board-page .board-link {
text-align: center;
font-size: 13px;
margin: 16px 0 28px;
}
body.mode-viewer {
background: #fff;
}
body.mode-viewer .topbar,
body.mode-viewer #editor-panel,
body.mode-viewer .preview-toolbar {
display: none;
}
body.mode-viewer main {
padding: 0;
}
body.mode-viewer .preview-band {
margin: 0;
padding: 0;
border: 0;
}
body.mode-viewer .flyer-preview {
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 0 0 20px;
overflow: auto;
}
body.mode-viewer .viewer-floating {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
width: 100%;
padding: 12px;
background: #fff;
}
body.mode-viewer .viewer-floating button {
min-height: 34px;
padding: 6px 10px;
font-size: 12px;
}
body.mode-viewer .viewer-floating select {
width: auto;
min-height: 34px;
max-width: 180px;
padding: 6px 8px;
font-size: 12px;
background: #fff;
}
body.mode-viewer #viewer-panel {
display: none;
}
body.mode-viewer #viewer-panel.drawer-open {
position: fixed;
top: 56px;
right: 12px;
bottom: 12px;
z-index: 19;
display: block;
width: min(680px, calc(100vw - 24px));
overflow: auto;
padding: 16px;
border: 2px solid #000;
background: #fff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
body.mode-viewer #viewer-panel.drawer-open .workspace {
display: block;
}
body.mode-viewer #viewer-panel.drawer-open .result-panel {
margin-top: 18px;
}
@media (max-width: 900px) {
body {
font-size: 16px;
}
.topbar {
display: block;
padding: 14px;
}
.topbar h1 {
font-size: 20px;
}
.topbar p {
margin-top: 4px;
font-size: 13px;
}
.topbar-controls {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 8px;
margin-top: 12px;
}
.topbar-controls select {
width: 100%;
max-width: none;
}
.mode-tabs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
margin-top: 0;
}
.tab {
min-height: 44px;
}
main {
padding: 12px;
}
.workspace,
.field-grid {
display: block;
}
.section-title,
.preview-toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 8px;
}
.identity-row {
align-items: stretch;
}
.identity-row .state-pill,
.identity-row .secondary-button {
width: 100%;
}
input,
select,
textarea {
min-height: 44px;
padding: 10px;
font-size: 16px;
}
textarea {
min-height: 120px;
}
.actions {
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.actions button,
.preview-toolbar button {
width: 100%;
min-height: 44px;
}
.result-panel {
margin-top: 18px;
}
.result-list {
display: block;
}
.result-list dt {
margin-top: 10px;
}
.copy-value {
grid-template-columns: minmax(0, 1fr);
}
.copy-button {
width: 100%;
min-height: 36px;
}
.preview-band {
margin-top: 18px;
padding-top: 12px;
}
.flyer-preview {
--mobile-flyer-scale: var(--flyer-preview-scale, 0.58);
width: 100%;
min-height: var(--flyer-preview-height, calc(1056px * var(--mobile-flyer-scale)));
overflow: hidden;
padding: 0;
}
.flyer-sheet,
.container {
/* Keep the sheet's true print width so the scale math (availableWidth /
sheetWidth) is right. In viewer mode .flyer-preview is a flexbox, and
without flex:none the sheet shrinks to the viewport, making the scale
compute to 1 and the full-width content clip instead of shrinking. */
flex: 0 0 auto;
transform: scale(var(--mobile-flyer-scale));
transform-origin: top left;
margin-bottom: var(--flyer-preview-collapse, calc((1056px * var(--mobile-flyer-scale)) - 1056px));
}
body.mode-viewer .viewer-floating {
position: sticky;
top: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
padding: 8px;
}
body.mode-viewer .viewer-floating button,
body.mode-viewer .viewer-floating select {
width: 100%;
max-width: none;
min-height: 40px;
font-size: 12px;
}
body.mode-viewer .viewer-floating select {
grid-column: 1 / -1;
}
body.mode-viewer .flyer-preview {
justify-content: flex-start;
min-height: 0;
padding: 0;
overflow: hidden;
}
body.mode-viewer #viewer-panel.drawer-open {
top: 96px;
right: 8px;
bottom: 8px;
width: calc(100vw - 16px);
padding: 12px;
}
}
@media print {
/* @page size is injected by applyPaperSize() in nostr-client.js (Letter or
A4) since @page cannot read CSS custom properties across engines. This
rule is the margin fallback and the default size before JS runs. */
@page {
size: Letter portrait;
margin: 0;
}
html,
body {
width: var(--sheet-w);
height: var(--sheet-h);
margin: 0;
padding: 0;
overflow: hidden;
}
body > header,
body > .viewer-floating,
.viewer-floating,
.viewer-floating *,
.board-link,
.modal-overlay,
#open-viewer-controls,
#viewer-print,
#viewer-editor,
#viewer-lang,
body > main > .panel,
.preview-toolbar,
.viewer-drawer {
display: none !important;
}
main {
display: block;
width: var(--sheet-w);
height: var(--sheet-h);
padding: 0;
margin: 0;
}
.preview-band {
display: block;
width: var(--sheet-w);
height: var(--sheet-h);
border: 0;
margin: 0;
padding: 0;
}
.flyer-preview {
display: block;
width: var(--sheet-w);
height: var(--sheet-h);
overflow: visible;
padding: 0;
margin: 0;
}
.flyer-sheet,
.container {
position: absolute;
inset: 0 auto auto 0;
width: var(--sheet-w);
height: var(--sheet-h);
transform: none;
margin: 0;
border: 0;
}
}
/* ---- Bulletin board page (board.html) ---- */
.board-main {
max-width: 1000px;
margin: 0 auto;
padding: 24px 20px 48px;
}
.board-back {
color: var(--accent);
text-decoration: none;
font-weight: 700;
}
#board-status {
color: var(--muted);
margin: 8px 0 16px;
}
.board-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.board-table th,
.board-table td {
text-align: left;
padding: 8px 10px;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
.board-table thead th {
position: sticky;
top: 0;
background: var(--soft);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.board-table thead th .sort-indicator {
color: var(--muted);
font-size: 11px;
}
.board-table tbody tr:hover {
background: #faf7f7;
}
.board-table .board-title {
font-weight: 700;
overflow-wrap: anywhere;
}
.board-table .board-npub {
font-family: "Courier New", monospace;
font-size: 12px;
white-space: nowrap;
}
.board-table .board-author-name {
font-weight: 700;
overflow-wrap: anywhere;
}
.board-table .board-date {
white-space: nowrap;
color: var(--muted);
}
.board-table .board-eventid {
font-family: "Courier New", monospace;
font-size: 12px;
white-space: nowrap;
}
.board-table .board-copy {
margin-inline-start: 6px;
font: inherit;
font-size: 11px;
padding: 2px 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--accent);
cursor: pointer;
white-space: nowrap;
}
/* Per-row management actions (edit / re-broadcast / delete / block) + the
"blocked" note in the filter bar. */
.board-action {
display: inline-block;
font: inherit;
font-size: 11px;
padding: 2px 8px;
margin: 0 2px;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--accent);
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.board-action.danger:hover {
background: var(--accent);
color: #fff;
}
.board-blocked-note {
font-size: 12px;
color: var(--muted);
}
/* Per-row ⋯ actions menu. */
.board-table td:last-child { text-align: right; }
.board-menu-wrap {
position: relative;
display: inline-block;
}
.board-menu-btn {
font: inherit;
font-size: 16px;
line-height: 1;
padding: 2px 8px;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--muted);
cursor: pointer;
}
.board-menu-btn:hover {
color: var(--accent);
border-color: var(--accent);
}
.board-menu {
position: absolute;
top: calc(100% + 4px);
inset-inline-end: 0;
z-index: 100;
min-width: 150px;
padding: 4px;
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);
display: flex;
flex-direction: column;
text-align: start;
}
.board-menu[hidden] { display: none; }
.board-menu-item {
display: block;
width: 100%;
box-sizing: border-box;
font: inherit;
font-size: 13px;
text-align: start;
padding: 7px 10px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--ink, #1a1a1a);
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.board-menu-item:hover {
background: var(--soft);
}
.board-menu-item.danger {
color: var(--accent);
}
/* The mobile "Details" disclosure toggle is hidden on desktop (the secondary
fields are their own columns there). */
.board-details-toggle {
display: none;
}
/* Narrow screens: the 7-column table becomes a stacked card per flyer. Title +
the ⋯ menu are always shown; the rest sits behind a per-row "Details" toggle. */
@media (max-width: 640px) {
.board-table thead { display: none; }
.board-table,
.board-table tbody,
.board-table tr,
.board-table td { display: block; width: auto; }
.board-table tr {
position: relative;
border: 1px solid var(--line);
border-radius: 8px;
margin: 0 0 10px;
padding: 10px 44px 10px 12px;
}
.board-table td {
border: none;
padding: 2px 0;
white-space: normal;
}
.board-table td[data-col="title"] { padding-top: 0; }
.board-table td[data-col="title"] .board-title { font-size: 15px; }
.board-table td[data-col="actions"] {
position: absolute;
top: 8px;
inset-inline-end: 8px;
padding: 0;
}
.board-details-toggle {
display: block;
width: fit-content;
margin-top: 4px;
padding: 2px 0;
font: inherit;
font-size: 12px;
background: transparent;
border: none;
color: var(--accent);
cursor: pointer;
user-select: none;
}
/* Secondary fields collapsed until the row is expanded. */
.board-table td[data-col="link"],
.board-table td[data-col="author"],
.board-table td[data-col="language"],
.board-table td[data-col="date"],
.board-table td[data-col="id"] {
display: none;
font-size: 13px;
}
.board-table tr.expanded td[data-col="link"],
.board-table tr.expanded td[data-col="author"],
.board-table tr.expanded td[data-col="language"],
.board-table tr.expanded td[data-col="date"],
.board-table tr.expanded td[data-col="id"] {
display: block;
}
/* Label each revealed field with its column name. */
.board-table tr.expanded td[data-col="link"]::before,
.board-table tr.expanded td[data-col="author"]::before,
.board-table tr.expanded td[data-col="language"]::before,
.board-table tr.expanded td[data-col="date"]::before,
.board-table tr.expanded td[data-col="id"]::before {
content: attr(data-label) ": ";
color: var(--muted);
font-weight: 700;
}
}
.board-table a {
color: var(--accent);
}
/* ---- Bulletin board login gate (NIP-07) ---- */
.board-gate {
max-width: 520px;
margin: 48px auto;
text-align: center;
}
.board-gate h2 {
margin: 0 0 8px;
font-size: 22px;
}
.board-gate p {
color: var(--muted);
margin: 0 0 20px;
line-height: 1.4;
}
.board-connect {
font: inherit;
font-weight: 700;
padding: 10px 24px;
border: none;
border-radius: 6px;
background: var(--accent);
color: #fff;
cursor: pointer;
}
.board-connect:disabled {
opacity: 0.6;
cursor: default;
}
.board-gate-error {
color: #8a4d00;
margin: 16px 0 0;
}
.board-connect-methods {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.board-connect-alt {
font: inherit;
font-size: 14px;
padding: 8px 18px;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--accent);
cursor: pointer;
}
.board-nsec-row,
.board-generated {
max-width: 420px;
margin: 18px auto 0;
text-align: left;
}
.board-nsec-row input,
.board-key-field input {
width: 100%;
box-sizing: border-box;
font: inherit;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 6px;
}
.board-nsec-row .board-connect {
margin-top: 8px;
}
.board-key-note {
color: var(--muted);
font-size: 12px;
margin: 8px 0 0;
}
.board-key-warning {
color: #8a4d00;
font-size: 13px;
margin: 0 0 12px;
line-height: 1.4;
}
.board-key-field {
display: block;
font-size: 12px;
color: var(--muted);
margin: 0 0 10px;
}
.board-key-field input {
margin-top: 4px;
font-family: "Courier New", monospace;
}
.board-key-secret {
display: flex;
gap: 6px;
align-items: stretch;
}
.board-key-secret input {
flex: 1;
}
.board-identity {
display: inline-flex;
align-items: center;
gap: 8px;
}
/* The `hidden` attribute must win over the display rule above. */
.board-identity[hidden] {
display: none;
}
.board-identity .board-npub {
font-family: "Courier New", monospace;
font-size: 12px;
color: var(--muted);
}
.board-disconnect {
font: inherit;
font-size: 13px;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--accent);
cursor: pointer;
}
/* Web-of-trust filter bar above the board table. */
.board-filter {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin: 4px 0 12px;
}
.board-filter-note {
color: var(--muted);
font-size: 13px;
}
.board-show-all {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
}
/* ---- Publish confirmation modal ---- */
.modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.5);
}
.modal-overlay[hidden] {
display: none;
}
.modal {
background: #fff;
color: var(--ink);
width: 100%;
max-width: 440px;
padding: 24px;
border-radius: 8px;
border: 1px solid var(--line);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
.modal h2 {
margin: 0 0 8px;
font-size: 20px;
}
.modal p {
margin: 0 0 14px;
color: var(--muted);
}
.modal-url-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
}
.modal-url-row input {
flex: 1;
min-width: 0;
padding: 8px 10px;
font-family: "Courier New", monospace;
font-size: 13px;
border: 1px solid var(--line);
border-radius: 4px;
}
.modal-url-row button {
white-space: nowrap;
}
.board-report-category,
#board-report-reason {
display: block;
width: 100%;
box-sizing: border-box;
margin: 0 0 12px;
padding: 8px 10px;
font: inherit;
border: 1px solid var(--line);
border-radius: 6px;
}
.modal-relays {
margin: 10px 0 0;
font-size: 13px;
color: var(--muted);
}
.modal-relays.modal-relays-warn {
color: var(--accent);
font-weight: 700;
}
.modal-close {
display: block;
margin-left: auto;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
/* Destructive action (delete). */
.secondary-button.danger {
border-color: var(--accent);
color: var(--accent);
}
.secondary-button.danger:hover {
background: var(--accent);
color: #fff;
}
#loaded-flyer-status {
color: var(--muted);
}
/* --- Safety & privacy page ------------------------------------------------ */
.safety-main {
max-width: 760px;
margin: 0 auto;
padding: 24px 20px 64px;
line-height: 1.55;
}
.safety-intro {
font-size: 16px;
margin: 0 0 28px;
}
.safety-section {
margin: 0 0 28px;
padding: 16px 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--soft);
}
.safety-section h2 {
margin: 0 0 12px;
font-size: 18px;
}
.safety-section ul {
margin: 0;
padding-inline-start: 22px;
}
.safety-section li {
margin: 0 0 10px;
}
.safety-section li:last-child {
margin-bottom: 0;
}
/* The "what it does NOT protect" block gets a warning accent so it reads as
the most important section on the page. */
.safety-exposes {
border-color: #d9a300;
background: #fff8e6;
}
.safety-closing {
font-style: italic;
color: var(--muted);
margin: 0;
}
.safety-page .topbar-controls {
gap: 10px;
}