diff --git a/static/style.css b/static/style.css index 51753ee..1947234 100644 --- a/static/style.css +++ b/static/style.css @@ -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; @@ -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); @@ -133,6 +150,16 @@ body { flex-direction: column; } +/* The
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; @@ -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; @@ -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; } @@ -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; @@ -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; @@ -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 { @@ -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; } diff --git a/templates/base.html b/templates/base.html index 8ef9421..97716b0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -78,7 +78,9 @@ +
{% block page %}{% endblock %} +
{% block scripts %}{% endblock %} diff --git a/templates/index.html b/templates/index.html index d1b9702..9517787 100644 --- a/templates/index.html +++ b/templates/index.html @@ -28,8 +28,8 @@

- Language - {% for lang in languages %}{% if lang %} @@ -50,10 +50,10 @@

-
Expiration
+
Expiration
Content permanently deleted after this delay.
- {% for value, label in ttl_options.items() %} {% endfor %} @@ -62,60 +62,60 @@

-
Max views
+
Max views
Delete after N reads. Leave empty for unlimited.
- +
-
Burn after read
+
Burn after read
Deleted permanently after the first view.
-
Compress before encrypting
+
Compress before encrypting
Gzip the plaintext locally before AES-GCM. 60–80% smaller for text/JSON/code.
-
Password protection
+
Password protection
Content is encrypted with your password.
- + - +
- Key derivation - @@ -124,17 +124,17 @@

-
Webhook on read
+
Webhook on read
POST to this URL each time the paste is viewed.
- +

diff --git a/tests/test_api.py b/tests/test_api.py index bebee6c..1d67e0c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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}"