Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 41 additions & 9 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
--drac-yellow: #f1fa8c;
--drac-orange: #ffb86c;

/* Action-chip text. The raw Dracula hues are built for dark backgrounds; on
a light chip they fall to ~1.1:1, which is invisible rather than merely
low-contrast. The light theme overrides them with darkened equivalents
that keep each action's hue identity and clear AA (all >= 5.4:1). */
--action-copy: var(--drac-green);
--action-raw: var(--drac-orange);
--action-download: var(--drac-yellow);
--paste-id-fg: var(--text-muted);
--notice-info-fg: var(--drac-cyan);
--notice-success-fg: var(--drac-green);

/* Ghostbit brand — Cyber-Violet */
--brand-primary: #BC13FE;
--brand-light: #D067FF;
Expand Down Expand Up @@ -93,6 +104,12 @@
--accent-dim: rgba(188, 19, 254, 0.10);
--danger: #c43;
--danger-hover: #a22;
--action-copy: #0a5f2a; /* 5.5:1 on the light copy chip */
--action-raw: #7d3f00; /* 5.7:1 on the light raw chip */
--action-download: #5f5300; /* 5.6:1 on the light download chip */
--paste-id-fg: #4a5169; /* 5.6:1, replaces a 2.5:1 blend */
--notice-info-fg: #0b4d5c; /* 9.1:1, was 1.3:1 */
--notice-success-fg: #0a5f2a; /* 7.6:1, was 1.3:1 */
--toggle-off: #c3c9d8;
--toggle-on: #BC13FE;
--shadow: 0 4px 24px rgba(40, 44, 60, 0.10);
Expand Down Expand Up @@ -133,6 +150,16 @@ body {
flex-direction: column;
}

/* The <main> landmark wraps the page block. body is a flex column and
.page-bg/.page-plain claim `flex: 1` as its children, so main forwards both
the growth and the axis — it exists for assistive tech, not for layout. */
main {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}

/* ── Page backgrounds ───────────────────────────────────────────────────────── */
.page-bg {
flex: 1;
Expand Down Expand Up @@ -605,7 +632,7 @@ select:focus { border-color: var(--border-focus); }
margin-top: 8px;
font-size: 0.72rem;
font-family: var(--font-ui);
color: var(--muted);
color: var(--text-muted);
}
.pw-strength-bar {
flex: 1;
Expand Down Expand Up @@ -782,19 +809,19 @@ kbd {
}
.notice svg { flex-shrink: 0; margin-top: 1px; }
.notice-warning { background: rgba(241,250,140,0.07); border-color: rgba(241,250,140,0.3); color: var(--drac-yellow); }
.notice-info { background: rgba(139,233,253,0.07); border-color: rgba(139,233,253,0.25); color: var(--drac-cyan); }
.notice-info { background: rgba(139,233,253,0.07); border-color: rgba(139,233,253,0.25); color: var(--notice-info-fg); }
.notice-error { background: rgba(255,85,85,0.08); border-color: rgba(255,85,85,0.3); color: #ff6e6e; }

.owner-notice { align-items: center; gap: 14px; }
.owner-notice .owner-notice-text { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.owner-notice .owner-notice-text strong { color: var(--text); font-weight: 600; }
.owner-notice .owner-notice-sub { color: var(--muted); font-size: 0.8rem; }
.owner-notice .owner-notice-sub { color: var(--text-muted); font-size: 0.8rem; }
.owner-notice.just-created {
background: rgba(80,250,123,0.08);
border-color: rgba(80,250,123,0.35);
color: var(--drac-green);
color: var(--notice-success-fg);
}
.owner-notice.just-created .owner-notice-text strong { color: var(--drac-green); }
.owner-notice.just-created .owner-notice-text strong { color: var(--notice-success-fg); }
.btn-owner-copy { flex-shrink: 0; padding: 8px 14px; font-size: 0.8rem; }
@media (max-width: 560px) {
.owner-notice { flex-wrap: wrap; }
Expand Down Expand Up @@ -883,7 +910,7 @@ kbd {
.paste-id {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text-muted);
color: var(--paste-id-fg);
padding-right: 10px;
border-right: 1px solid var(--border);
margin-right: 2px;
Expand Down Expand Up @@ -935,13 +962,13 @@ kbd {
.btn-action-copy {
background: rgba(80, 250, 123, 0.1);
border-color: rgba(80, 250, 123, 0.3);
color: var(--drac-green);
color: var(--action-copy);
}
.btn-action-copy:hover { background: rgba(80, 250, 123, 0.2); }
.btn-action-raw {
background: rgba(255, 184, 108, 0.1);
border-color: rgba(255, 184, 108, 0.3);
color: var(--drac-orange);
color: var(--action-raw);
display: inline-flex;
align-items: center;
gap: 5px;
Expand All @@ -950,7 +977,7 @@ kbd {
.btn-action-download {
background: rgba(241, 250, 140, 0.1);
border-color: rgba(241, 250, 140, 0.3);
color: var(--drac-yellow);
color: var(--action-download);
}
.btn-action-download:hover { background: rgba(241, 250, 140, 0.2); }
.btn-action-delete {
Expand Down Expand Up @@ -1586,3 +1613,8 @@ kbd {
[data-theme="light"] .logo:hover img {
filter: brightness(0.65) saturate(1.6) var(--logo-img-glow-hover);
}

/* --text-muted at 0.7 opacity blends to 2.5:1 against the light inbar. The
light theme carries its own already-muted tone, so the opacity is dropped
there rather than stacking two forms of dimming. */
[data-theme="light"] .paste-id { opacity: 1; }
2 changes: 2 additions & 0 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@
</div>
</header>

<main>
{% block page %}{% endblock %}
</main>

{% block scripts %}{% endblock %}
<script src="/static/theme.js?v={{ v }}" defer></script>
Expand Down
38 changes: 19 additions & 19 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ <h1 class="terminal-hero">
</div>
<span class="char-counter" id="charCounter"></span>
<div class="lang-picker-wrap">
<span class="lang-picker-label">Language</span>
<select name="language" id="language" class="lang-picker">
<span class="lang-picker-label" id="langPickerLabel">Language</span>
<select name="language" id="language" class="lang-picker" aria-labelledby="langPickerLabel">
<option value="">Plain text</option>
{% for lang in languages %}{% if lang %}
<option value="{{ lang }}">{{ lang }}</option>
Expand All @@ -50,10 +50,10 @@ <h1 class="terminal-hero">

<div class="option-row option-row-column">
<div class="option-text">
<div class="option-label">Expiration</div>
<div class="option-label" id="expiresLabel">Expiration</div>
<div class="option-desc">Content permanently deleted after this delay.</div>
</div>
<select name="expires_in" id="expires_in">
<select name="expires_in" id="expires_in" aria-labelledby="expiresLabel">
{% for value, label in ttl_options.items() %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
Expand All @@ -62,60 +62,60 @@ <h1 class="terminal-hero">

<div class="option-row option-row-column">
<div class="option-text">
<div class="option-label">Max views</div>
<div class="option-label" id="maxViewsLabel">Max views</div>
<div class="option-desc">Delete after N reads. Leave empty for unlimited.</div>
</div>
<div class="number-wrap">
<button type="button" class="number-btn number-btn-minus" data-target="max_views" data-dir="-1" aria-label="Decrease">−</button>
<input type="number" name="max_views" id="max_views" min="1" placeholder="∞" class="input-small">
<input type="number" name="max_views" id="max_views" min="1" placeholder="∞" class="input-small" aria-labelledby="maxViewsLabel">
<button type="button" class="number-btn number-btn-plus" data-target="max_views" data-dir="1" aria-label="Increase">+</button>
</div>
</div>

<div class="option-row">
<div class="option-text">
<div class="option-label">Burn after read</div>
<div class="option-label" id="burnLabel">Burn after read</div>
<div class="option-desc">Deleted permanently after the first view.</div>
</div>
<label class="toggle">
<input type="checkbox" name="burn" value="true" id="burnToggle">
<input type="checkbox" name="burn" value="true" id="burnToggle" aria-labelledby="burnLabel">
<span class="toggle-track"></span>
</label>
</div>

<div class="option-row">
<div class="option-text">
<div class="option-label">Compress before encrypting</div>
<div class="option-label" id="compressLabel">Compress before encrypting</div>
<div class="option-desc">Gzip the plaintext locally before AES-GCM. 60–80% smaller for text/JSON/code.</div>
</div>
<label class="toggle">
<input type="checkbox" id="compressToggle">
<input type="checkbox" id="compressToggle" aria-labelledby="compressLabel">
<span class="toggle-track"></span>
</label>
</div>

<div class="option-row option-row-password">
<div class="option-text">
<div class="option-label">Password protection</div>
<div class="option-label" id="passwordLabel">Password protection</div>
<div class="option-desc">Content is encrypted with your password.</div>
</div>
<label class="toggle">
<input type="checkbox" id="passwordToggle">
<input type="checkbox" id="passwordToggle" aria-labelledby="passwordLabel">
<span class="toggle-track"></span>
</label>
</div>

<div class="password-input-wrap" id="passwordWrap">
<input type="password" name="password" id="password" placeholder="Enter a password…" autocomplete="new-password">
<input type="password" name="password" id="password" placeholder="Enter a password…" autocomplete="new-password" aria-label="Password">
<div class="pw-strength" id="pwStrength" aria-hidden="true">
<div class="pw-strength-bar"><div class="pw-strength-fill" id="pwStrengthFill"></div></div>
<span class="pw-strength-label" id="pwStrengthLabel">—</span>
</div>
<input type="password" id="passwordConfirm" placeholder="Confirm password…" autocomplete="new-password" class="pw-confirm">
<input type="password" id="passwordConfirm" placeholder="Confirm password…" autocomplete="new-password" class="pw-confirm" aria-label="Confirm password">
<div class="pw-match" id="pwMatch" aria-live="polite"></div>
<div class="kdf-picker-wrap">
<span class="kdf-picker-label">Key derivation</span>
<select id="kdfChoice" class="kdf-picker">
<span class="kdf-picker-label" id="kdfPickerLabel">Key derivation</span>
<select id="kdfChoice" class="kdf-picker" aria-labelledby="kdfPickerLabel">
<option value="pbkdf2-sha256">PBKDF2-SHA256 (600k iters)</option>
<option value="argon2id">Argon2id (memory-hard, recommended)</option>
</select>
Expand All @@ -124,17 +124,17 @@ <h1 class="terminal-hero">

<div class="option-row option-row-webhook">
<div class="option-text">
<div class="option-label">Webhook on read</div>
<div class="option-label" id="webhookLabel">Webhook on read</div>
<div class="option-desc">POST to this URL each time the paste is viewed.</div>
</div>
<label class="toggle">
<input type="checkbox" id="webhookToggle">
<input type="checkbox" id="webhookToggle" aria-labelledby="webhookLabel">
<span class="toggle-track"></span>
</label>
</div>

<div class="webhook-input-wrap" id="webhookWrap">
<input type="url" name="webhook_url" id="webhook_url" placeholder="https://…" autocomplete="off">
<input type="url" name="webhook_url" id="webhook_url" placeholder="https://…" autocomplete="off" aria-label="Webhook URL">
</div>

</div>
Expand Down
17 changes: 17 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,3 +550,20 @@ async def test_plaintext_paste_skips_the_codemirror_mode_bundle(client):

py = (await client.post("/api/v1/pastes", json=_fake_paste(language="python"))).json()["id"]
assert "codemirror-modes.min.js" in (await client.get(f"/{py}")).text


def test_css_defines_every_custom_property_it_uses():
"""An undefined var() is invalid at computed-value time, so the declaration
is dropped and the element silently inherits its parent's value instead.
That is how `.owner-notice-sub` ended up rendering in Dracula cyan for
months: it asked for var(--muted), which never existed."""
import re
from pathlib import Path

css = (Path(__file__).resolve().parent.parent / "static" / "style.css").read_text()
defined = set(re.findall(r"(--[\w-]+)\s*:", css))
# var(--x, fallback) is legitimate even when --x is undefined; only bare
# references are a bug.
used = set(re.findall(r"var\(\s*(--[\w-]+)\s*\)", css))
missing = sorted(used - defined)
assert not missing, f"style.css uses undefined custom properties: {missing}"