Files
VoxVera/site/nostr-client.css
T
thePR0M3TH3ANandClaude Opus 4.8 a4cb8dffcb Add NIP-46 remote signer (bunker) as an editor identity
The editor can now sign flyers via a NIP-46 remote signer: paste a
bunker:// link and the user's secret stays in their signer app
(nsec.app, Amber, …) while the browser holds only an ephemeral local
key. The vendored nostr-tools bundle has no nip46 module, so the client
is hand-rolled over a raw WebSocket — kind-24133 JSON-RPC encrypted with
NIP-44 (with a NIP-04 decrypt fallback), running connect →
get_public_key → sign_event. Connections are session-only; the bunker
token is never written to disk, so a reload returns to anonymous.

Switching to/from any other identity mode disconnects the signer, and a
successful connection mirrors the user pubkey to the board's connected
key. Adds 9 localized strings across all 14 languages (i18n parity test
passes).

Tested with a fake relay/signer doing real NIP-44 crypto: full connect →
get_public_key → sign_event → publish round-trip, across all 6 engines.
Full suite 312 passing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-11 13:17:17 -04:00

1629 lines
28 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;
}
/* 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);
}
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;
}
.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);
}