mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 11:12:29 +00:00
- Upload images (png/jpeg/webp/gif ≤5 MiB) alongside the text of a billet: multi-image mini-gallery. New api/services/media.py validates + FULLY re-encodes every upload via Pillow (drops EXIF/GPS, neutralises polyglots; SVG refused), producing a cleaned original + a ≤480px thumbnail. - Storage under BILLETS_MEDIA_DIR (/var/lib/secubox/billets/media), served by nginx /media/ alias (in-process StaticFiles fallback). img-src 'self' already covers it — no third party, no CSP relaxation. - Public: vignette grid in feed + permalink, pure-JS zoomable lightbox with ◀▶ keyboard/arrow navigation (graceful degradation: links open the full image with JS off). First image drives og:image / twitter:image. - Admin editor: multipart file input + existing-media thumbnails with delete. New media table (migration 0002, ON DELETE CASCADE), repo CRUD, media delete route; billet delete removes files. All Pillow work off the event loop (asyncio.to_thread). - Portable backup: GET /admin/export.sbxsite emits every billet + media inlined as base64 (single re-importable file). - nginx client_max_body_size 6m; requirements pillow==11.1.0. 104 tests (11 new). Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
160 lines
9.4 KiB
CSS
160 lines
9.4 KiB
CSS
/* SPDX-License-Identifier: LicenseRef-CMSD-1.0 */
|
|
/* billets — funky / party public theme, theme-aware, emoji-friendly */
|
|
:root{
|
|
--bg:#fff7fb; --fg:#1c1330; --muted:#7a6a90; --card:#ffffff;
|
|
--border:#f0e2f2; --link:#c0246b;
|
|
--p1:#7c3aed; --p2:#ec4899; --p3:#06b6d4; --p4:#f59e0b;
|
|
--grad:linear-gradient(120deg,#7c3aed,#ec4899 45%,#f59e0b);
|
|
}
|
|
@media (prefers-color-scheme:dark){
|
|
:root{ --bg:#0d0a18; --fg:#f0ecff; --muted:#9a8ec2; --card:#171226;
|
|
--border:#271d3f; --link:#ff8ac4;
|
|
--grad:linear-gradient(120deg,#a855f7,#ec4899 45%,#22d3ee); }
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--bg);color:var(--fg);
|
|
font:16px/1.65 "Segoe UI",system-ui,-apple-system,Roboto,sans-serif;
|
|
max-width:46rem;margin-inline:auto;padding:0 1rem;overflow-x:hidden}
|
|
body::before{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
|
|
background:
|
|
radial-gradient(60vw 40vw at 90% -5%,rgba(236,72,153,.16),transparent 60%),
|
|
radial-gradient(55vw 40vw at -10% 10%,rgba(124,58,237,.16),transparent 60%),
|
|
radial-gradient(50vw 40vw at 50% 110%,rgba(6,182,212,.13),transparent 60%)}
|
|
a{color:var(--link);text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
|
|
/* header */
|
|
.site-header{display:flex;align-items:center;gap:.7rem;padding:1.5rem 0 1rem;flex-wrap:wrap}
|
|
.site-title{font-weight:900;font-size:1.7rem;letter-spacing:-.02em;
|
|
background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
.site-title::before{content:"📮 "}
|
|
.tagline{color:var(--muted);font-size:.85rem}
|
|
.tagline::after{content:" ✨"}
|
|
|
|
/* billet cards */
|
|
.billet{position:relative;background:var(--card);border:1px solid var(--border);
|
|
border-radius:20px;padding:1.15rem 1.3rem;margin-bottom:1.15rem;
|
|
box-shadow:0 10px 30px rgba(124,58,237,.07);transition:transform .16s,box-shadow .16s}
|
|
.billet:hover{transform:translateY(-3px);box-shadow:0 16px 40px rgba(236,72,153,.13)}
|
|
.billet::before{content:"";position:absolute;left:0;top:16px;bottom:16px;width:4px;
|
|
border-radius:4px;background:var(--grad)}
|
|
.billet .e-content>:first-child{margin-top:0}
|
|
.billet .e-content>:last-child{margin-bottom:0}
|
|
.billet-full{padding:1.5rem 1.7rem}
|
|
blockquote{border-left:3px solid var(--p2);margin:.6rem 0;padding:.2rem 0 .2rem 1rem;color:var(--muted)}
|
|
code{background:color-mix(in srgb,var(--p1) 14%,transparent);padding:.1em .4em;border-radius:6px;font-size:.9em}
|
|
.billet-meta{display:flex;gap:.8rem;align-items:center;color:var(--muted);font-size:.8rem;margin-top:.7rem;flex-wrap:wrap}
|
|
.billet-meta .u-url::before{content:"🔗 "}
|
|
|
|
/* inline embedded link chip */
|
|
.ref-chip{display:inline-flex;align-items:center;gap:.4rem;margin:.5rem 0;padding:.4rem .8rem;
|
|
border-radius:999px;background:color-mix(in srgb,var(--p3) 12%,transparent);
|
|
border:1px solid color-mix(in srgb,var(--p3) 40%,transparent);font-size:.85rem;font-weight:600}
|
|
.ref-chip::before{content:"🌐"}
|
|
|
|
/* inline embedded media (oEmbed / link card) */
|
|
.embed{margin:.9rem 0}
|
|
.embed iframe{width:100%;max-width:100%;min-height:320px;border:0;border-radius:16px;
|
|
box-shadow:0 8px 26px rgba(0,0,0,.18);background:#000}
|
|
.embed blockquote{background:var(--card);border:1px solid var(--border);border-left:3px solid var(--p2);
|
|
border-radius:14px;padding:1rem;margin:0}
|
|
.link-card{display:flex;gap:.9rem;align-items:center;border:1px solid var(--border);
|
|
border-radius:16px;overflow:hidden;background:var(--card);text-decoration:none;color:var(--fg)}
|
|
.link-card:hover{border-color:var(--p2);text-decoration:none}
|
|
.link-card .card-img{width:120px;height:90px;object-fit:cover;flex:none}
|
|
.link-card .card-body{display:flex;flex-direction:column;gap:.15rem;padding:.6rem .3rem}
|
|
.link-card .card-title{font-weight:700}
|
|
.link-card .card-desc{color:var(--muted);font-size:.85rem}
|
|
.link-card .card-host{color:var(--p3);font-size:.75rem}
|
|
|
|
/* reactions */
|
|
.reactions{display:flex;flex-wrap:wrap;gap:.45rem;margin:.8rem 0}
|
|
.reactions form.react{margin:0}
|
|
.react-btn{cursor:pointer;background:var(--card);border:1.5px solid var(--border);border-radius:999px;
|
|
padding:.3rem .75rem;font-size:1.05rem;color:var(--fg);transition:transform .12s,border-color .12s}
|
|
.react-btn:hover{transform:scale(1.12) rotate(-4deg)}
|
|
.react-btn.on{border-color:var(--p2);background:color-mix(in srgb,var(--p2) 12%,transparent);font-weight:700}
|
|
.react-btn .count{color:var(--muted);font-size:.78rem;margin-left:.25rem}
|
|
.react-btn.on .count{color:var(--p2)}
|
|
|
|
/* quick share */
|
|
.share-quick{display:flex;flex-wrap:wrap;gap:.4rem;align-items:center;margin:.7rem 0}
|
|
.share-quick .lbl{color:var(--muted);font-size:.8rem;font-weight:700;margin-right:.1rem}
|
|
.share-quick a,.share-quick button{cursor:pointer;text-decoration:none;font-size:1.05rem;line-height:1;
|
|
border:1px solid var(--border);background:var(--card);color:var(--fg);border-radius:999px;
|
|
padding:.32rem .55rem;transition:transform .12s,border-color .12s}
|
|
.share-quick a:hover,.share-quick button:hover{transform:translateY(-2px) scale(1.06);border-color:var(--p1)}
|
|
|
|
/* comments */
|
|
.comments{margin-top:1.8rem;border-top:2px dashed var(--border);padding-top:1.1rem}
|
|
.comments h2::before{content:"💬 "}
|
|
.comment{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:.7rem 1rem;margin-bottom:.6rem}
|
|
.comment .c-meta{margin:0 0 .3rem;font-size:.85rem;color:var(--muted)}
|
|
.comment .c-meta strong::before{content:"🙂 "}
|
|
.comment .c-body{margin:0}
|
|
.comment-form{display:flex;flex-direction:column;gap:.55rem;margin-top:1rem}
|
|
.comment-form label{display:flex;flex-direction:column;gap:.25rem;font-size:.85rem;color:var(--muted)}
|
|
.comment-form input,.comment-form textarea{padding:.6rem;border:1.5px solid var(--border);border-radius:12px;
|
|
background:var(--bg);color:var(--fg);font:inherit}
|
|
.comment-form input:focus,.comment-form textarea:focus{outline:none;border-color:var(--p2)}
|
|
.comment-form button,.mod-actions button,.admin-form button,.editor-actions button,.row-actions button{
|
|
cursor:pointer;padding:.55rem 1rem;border:none;border-radius:12px;background:var(--grad);color:#fff;
|
|
font:inherit;font-weight:700}
|
|
.comment-form button:hover{transform:translateY(-2px)}
|
|
.danger{background:#e11d48!important}
|
|
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
|
|
.note{background:color-mix(in srgb,var(--p1) 13%,transparent);padding:.55rem .9rem;border-radius:12px}
|
|
.note::before{content:"🎉 "}
|
|
.note.err{background:color-mix(in srgb,#e11d48 15%,transparent)}
|
|
.note.err::before{content:"😅 "}
|
|
|
|
/* pager / footer / misc */
|
|
.pager{margin:1.4rem 0 2.5rem}
|
|
.pager a{font-weight:800;font-size:1.02rem}
|
|
.pager a::after{content:" 🕰️"}
|
|
.empty{color:var(--muted)}
|
|
.site-footer{border-top:1px solid var(--border);margin-top:2.2rem;padding:1rem 0 2.5rem;color:var(--muted);font-size:.8rem}
|
|
|
|
/* admin (kept compact) */
|
|
.admin-form{display:flex;flex-direction:column;gap:.6rem;max-width:34rem}
|
|
.admin-form label{display:flex;flex-direction:column;gap:.25rem}
|
|
.admin-form input,.admin-form textarea{padding:.55rem;border:1.5px solid var(--border);border-radius:12px;background:var(--bg);color:var(--fg);font:inherit}
|
|
.admin-list{width:100%;border-collapse:collapse}
|
|
.admin-list th,.admin-list td{text-align:left;padding:.45rem;border-bottom:1px solid var(--border);font-size:.9rem}
|
|
.row-actions{display:flex;gap:.4rem;align-items:center}
|
|
.mod-actions{display:flex;gap:.5rem;margin-top:.4rem}
|
|
.pill{font-size:.68rem;padding:.12rem .55rem;border-radius:999px;border:1px solid var(--border);text-transform:uppercase;font-weight:700}
|
|
.pill.published{color:#fff;background:var(--p1);border-color:transparent}
|
|
.pill.draft{color:var(--muted)}
|
|
.editor-actions{display:flex;gap:.5rem;flex-wrap:wrap}
|
|
.error{background:color-mix(in srgb,#e11d48 14%,transparent);padding:.55rem .9rem;border-radius:12px}
|
|
@media(max-width:480px){.link-card .card-img{width:84px;height:72px}}
|
|
|
|
/* ── media gallery + lightbox ─────────────────────────────────────────── */
|
|
.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
|
|
gap:.5rem;margin:.75rem 0}
|
|
.gallery-item{display:block;border-radius:8px;overflow:hidden;line-height:0;
|
|
border:1px solid rgba(255,255,255,.08);background:#0d1117}
|
|
.gallery-item img{width:100%;height:150px;object-fit:cover;cursor:zoom-in;
|
|
transition:transform .2s}
|
|
.gallery-item:hover img{transform:scale(1.04)}
|
|
/* single image: let it breathe, don't crop hard */
|
|
.gallery:has(.gallery-item:only-child) .gallery-item img{height:auto;max-height:520px;object-fit:contain}
|
|
|
|
.lightbox{position:fixed;inset:0;z-index:1000;display:none;
|
|
align-items:center;justify-content:center;background:rgba(5,8,12,.94);
|
|
backdrop-filter:blur(4px)}
|
|
.lightbox.open{display:flex}
|
|
body.lb-lock{overflow:hidden}
|
|
.lb-img{max-width:92vw;max-height:88vh;object-fit:contain;border-radius:6px;
|
|
box-shadow:0 20px 60px rgba(0,0,0,.6);cursor:zoom-in;transition:transform .2s}
|
|
.lb-img.zoomed{transform:scale(1.8);cursor:zoom-out}
|
|
.lb-close,.lb-nav{position:absolute;background:rgba(20,30,45,.7);color:#e8e6d9;
|
|
border:1px solid rgba(100,150,200,.3);cursor:pointer;font-family:inherit;
|
|
border-radius:8px;transition:border-color .15s,background .15s}
|
|
.lb-close:hover,.lb-nav:hover{border-color:#00d4ff;background:rgba(0,212,255,.15)}
|
|
.lb-close{top:1rem;right:1rem;width:2.4rem;height:2.4rem;font-size:1.1rem}
|
|
.lb-nav{top:50%;transform:translateY(-50%);width:2.8rem;height:3.4rem;font-size:1.8rem}
|
|
.lb-prev{left:1rem}.lb-next{right:1rem}
|
|
@media(max-width:600px){.lb-nav{width:2.2rem;height:2.8rem;font-size:1.4rem}}
|