Files
gerbeur/src/App.css
khannurien 6c4f410d9b
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
v3: added a chatbox
2026-06-29 20:25:10 +00:00

5570 lines
111 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.
/* ── Markdown prose ── */
.md p {
margin: 0 0 0.85em;
}
.md p:last-child {
margin-bottom: 0;
}
.md a {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.md a:hover {
text-decoration: none;
}
.md strong {
font-weight: 700;
}
.md em {
font-style: italic;
}
.md code {
font-family: var(--font-mono, monospace);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 3px;
padding: 0.1em 0.35em;
font-size: 0.88em;
}
.md pre {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.75rem 1rem;
overflow-x: auto;
margin: 0.6em 0;
}
.md pre code {
background: none;
border: none;
padding: 0;
}
.md ul, .md ol {
padding-left: 1.5em;
margin: 0.4em 0;
}
.md li {
margin: 0.15em 0;
}
.md blockquote {
border-left: 3px solid var(--color-border);
margin: 0.5em 0;
padding: 0.2em 0.75em;
opacity: 0.75;
}
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
margin: 0.6em 0 0.2em;
font-weight: 700;
line-height: 1.25;
}
.md img {
max-width: 100%;
height: auto;
display: block;
}
/* Compact / card mode: strip vertical spacing */
.md--inline p,
.md--inline ul,
.md--inline ol,
.md--inline pre,
.md--inline blockquote {
margin: 0;
}
.md--inline li {
margin: 0;
}
.md--inline ul,
.md--inline ol {
padding-left: 1.2em;
}
/* ── Dump detail page ── */
.dump-detail {
display: flex;
flex-direction: column;
gap: 1rem;
}
.dump-post-header {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1.25rem;
margin-bottom: 3px;
background: var(--color-surface);
border-radius: 12px 12px 0 0;
}
.dump-header-block {
display: flex;
align-items: center;
gap: 1rem;
}
.dump-header-left {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.dump-header-right {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.dump-title {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
line-height: 1.25;
word-break: break-word;
}
.dump-title-view {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.4rem;
border-radius: 6px;
max-width: 100%;
/* Padding gives the hover box breathing room (incl. right of the pencil);
the matching negative margin keeps the title's visual position unchanged. */
padding: 0.1rem 0.45rem;
margin: -0.1rem -0.45rem;
}
.dump-title-view--editable {
cursor: pointer;
}
.dump-title-view--editable:hover {
background: var(--color-surface);
}
.dump-title-edit-btn {
font-size: 0.85rem;
line-height: 1;
flex-shrink: 0;
color: var(--color-text-muted);
opacity: 0;
transition: opacity 0.1s;
pointer-events: none;
}
.dump-title-view--editable:hover .dump-title-edit-btn {
opacity: 1;
}
.dump-title-editor {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
/* Shared chrome for inline-edit inputs (the editors that live outside `.form`).
Each rule below adds only its own size / weight / width deltas. */
.dump-title-input,
.profile-email-input,
.playlist-edit-input,
.playlist-edit-textarea {
border: 2px solid var(--color-border);
border-radius: 8px;
background: var(--color-bg);
color: var(--color-text);
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.dump-title-input:focus,
.profile-email-input:focus,
.playlist-edit-input:focus,
.playlist-edit-textarea:focus {
border-color: var(--color-accent);
}
.dump-title-input {
flex: 1;
min-width: 12rem;
padding: 0.4rem 0.6rem;
font-size: 1.25rem;
font-weight: 700;
}
.dump-title-error {
width: 100%;
margin: 0;
font-size: 0.85rem;
color: var(--color-danger);
}
/* On the narrowest viewports the fixed-width action column squeezes the title
into a sliver. Stack the block: title takes the full width on top, the
vote/playlist actions sit in a row beneath it. */
@media (max-width: 512px) {
.dump-header-block {
flex-direction: column;
align-items: stretch;
}
.dump-header-left {
flex-direction: row;
align-items: center;
order: 2;
}
.dump-header-right {
order: 1;
}
}
.dump-comment {
font-size: 1.05rem;
line-height: 1.72;
opacity: 0.85;
border-left: 3px solid var(--color-accent);
margin: 0;
padding: 0.5rem 1rem;
word-break: break-word;
}
.dump-rich-content {
padding: 1.25rem;
background: var(--color-surface);
border-radius: 0 0 12px 12px;
}
.dump-url-link {
display: inline-block;
word-break: break-all;
padding: 1rem;
border: 2px solid var(--color-border);
border-radius: 8px;
width: 100%;
box-sizing: border-box;
}
.dump-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.dump-actions a {
color: var(--color-text);
text-decoration: none;
font-size: 0.9rem;
opacity: 0.7;
transition: opacity 0.15s, color 0.15s;
}
.dump-actions a:hover {
opacity: 1;
color: var(--color-accent);
}
.dump-actions a:last-child {
margin-left: auto;
}
/* ── Forms ── */
/* Unified form system: `.form` container + `.form-field` / `.form-label` /
`.form-hint` building blocks, shared by every form. */
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.form-label {
font-size: 0.85rem;
font-weight: 600;
opacity: 0.6;
}
/* Label paired with an inline action (e.g. a "reset to default" button). */
.form-label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.form-label-action {
background: none;
border: none;
padding: 0;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-text);
opacity: 0.6;
}
.form-label-action:hover:not(:disabled) {
opacity: 1;
}
.form-label-action:disabled {
opacity: 0.3;
cursor: default;
}
.form input,
.form textarea {
width: 100%;
box-sizing: border-box;
padding: 0.65rem 1rem;
border-radius: 8px;
border: 2px solid var(--color-border);
background-color: var(--color-bg);
color: var(--color-text);
font-size: 1rem;
font-family: var(--font-body);
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
line-height: 1.5;
transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
outline: none;
}
/* A submit/secondary button placed as a direct child of `.form` (the auth
forms, which have no action row) spans the full width like the inputs.
Buttons inside `.form-actions` keep their intrinsic width. */
.form > .btn-primary,
.form > .btn-secondary,
.form > .btn-danger {
width: 100%;
}
.form textarea {
resize: vertical;
}
.form input:hover,
.form textarea:hover {
border-color: color-mix(
in srgb,
var(--color-accent) 45%,
var(--color-border) 55%
);
}
.form input:focus,
.form textarea:focus {
border-color: var(--color-accent);
background-color: color-mix(
in srgb,
var(--color-accent) 4%,
var(--color-bg) 96%
);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-accent) 18%, transparent);
}
.form input:disabled,
.form textarea:disabled {
opacity: 0.6;
}
.form-hint {
font-size: 0.8rem;
opacity: 0.7;
margin-top: 0.1rem;
}
.form-hint--error {
color: var(--color-error, #c0392b);
opacity: 1;
}
.form-success {
color: var(--color-success, green);
margin: 0;
}
.dump-create-title-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.dump-create-title-row input {
flex: 1;
}
.dump-create-title-edit-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
line-height: 1;
color: var(--color-muted);
border-radius: 4px;
padding: 0.4rem;
transition: color 0.15s;
}
.dump-create-title-edit-btn:hover {
color: var(--color-text);
}
.dump-create-title {
margin: 0;
font-size: 1.4rem;
font-weight: 700;
}
/* ── Mode toggle — segmented control ── */
.visibility-toggle {
display: flex;
background: var(--color-bg);
border: 1.5px solid var(--color-border);
border-radius: 9px;
padding: 3px;
gap: 2px;
}
.visibility-toggle button {
flex: 1;
padding: 0.3rem 0.9rem;
border-radius: 6px;
border: none;
background: transparent;
color: var(--color-text);
cursor: pointer;
font-size: 0.88rem;
font-weight: 600;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.visibility-toggle button.active {
background: var(--color-accent);
color: var(--color-on-accent);
}
.visibility-toggle button:not(.active):hover {
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}
/* ── FileDropZone ── */
.fdz-wrapper {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.fdz-label {
font-size: 0.9rem;
font-weight: 500;
}
.fdz {
border: 2px dashed var(--color-border-subtle);
border-radius: 10px;
background: var(--color-surface);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
outline: none;
}
.fdz:hover:not(.fdz--disabled):not(.fdz--filled),
.fdz:focus-visible {
border-color: var(--color-accent);
}
.fdz--drag {
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface));
}
.fdz--disabled {
cursor: not-allowed;
opacity: 0.55;
}
.fdz--filled {
cursor: default;
border-style: solid;
}
.fdz__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
padding: 2rem 1.5rem;
user-select: none;
}
.fdz__upload-icon {
width: 2rem;
height: 2rem;
opacity: 0.4;
margin-bottom: 0.2rem;
}
.fdz__hint {
margin: 0;
font-size: 0.9rem;
font-weight: 500;
}
.fdz__browse {
margin: 0;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.fdz__browse-link {
color: var(--color-accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.fdz__limit {
margin: 0.25rem 0 0;
font-size: 0.78rem;
color: var(--color-text-muted);
}
.fdz__file {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem;
}
.fdz__file-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.fdz__file-meta {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
flex: 1;
}
.fdz__file-name {
font-size: 0.9rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fdz__file-size {
font-size: 0.78rem;
color: var(--color-text-muted);
}
.fdz__clear {
flex-shrink: 0;
background: none;
border: none;
cursor: pointer;
color: var(--color-text-muted);
font-size: 0.8rem;
padding: 0.3rem 0.5rem;
border-radius: 4px;
transition: color 0.12s, background 0.12s;
line-height: 1;
}
.fdz__clear:hover {
color: var(--color-text);
background: var(--color-border-subtle);
}
/* ── Local file / URL preview (DumpCreate) ── */
.local-preview-image {
width: 100%;
max-height: 320px;
object-fit: contain;
border-radius: 10px;
background: var(--color-bg);
display: block;
}
.preview-loading {
margin: 0;
font-size: 0.85rem;
opacity: 0.5;
font-style: italic;
}
/* File previews (Dump page) */
.file-preview-image {
max-width: 100%;
border-radius: 8px;
display: block;
}
.file-preview-play-btn {
position: relative;
width: 100%;
padding: 0;
border: none;
background: #000;
cursor: pointer;
display: block;
border-radius: 8px;
overflow: hidden;
}
.file-preview-video-thumb {
width: 100%;
max-height: 480px;
object-fit: contain;
display: block;
}
.file-preview-play-btn .rich-content-play-overlay {
font-size: 3rem;
}
.audio-file-preview {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: color-mix(
in srgb,
var(--color-accent) 8%,
var(--color-surface) 92%
);
border: 2px solid var(--color-border);
border-radius: 0 0 12px 12px;
}
/* When active, brighten only the *un-played* bars. Excluding played bars keeps
this rule from out-specifying `.waveform-bar--played` (which would paint every
bar the played colour and make the waveform look stuck at 100%). */
.audio-file-preview--active .waveform-bar:not(.waveform-bar--played) {
fill: color-mix(in srgb, var(--color-accent) 45%, var(--color-border) 55%);
}
.audio-file-preview--active .audio-player-btn {
background: var(--color-accent-hover);
}
/* ── Video player ── */
.video-player {
width: 100%;
border-radius: 0 0 12px 12px;
overflow: hidden;
border: 2px solid var(--color-border);
position: relative;
}
.video-player-video {
width: 100%;
max-height: 480px;
object-fit: contain;
display: block;
cursor: pointer;
}
.video-player:not(.video-player--controls-visible) .video-player-video {
cursor: none;
}
.video-player .video-player-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-radius: 0;
border: none;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.38));
color: var(--color-on-accent);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.video-player--controls-visible .video-player-controls {
opacity: 1;
pointer-events: auto;
}
.video-player-controls .audio-player-time {
color: var(--color-on-accent);
opacity: 0.85;
}
.video-player-controls .audio-player-vol-btn {
color: var(--color-on-accent);
}
.video-player-controls .audio-player-track {
background: rgba(255, 255, 255, 0.25);
}
.video-player-controls .audio-player-btn {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
}
.video-player-controls .audio-player-btn:hover {
background: rgba(255, 255, 255, 0.35);
}
/* ── Custom audio player ── */
.audio-player {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 0.6rem;
padding: 0.75rem 1rem;
background: color-mix(
in srgb,
var(--color-accent) 8%,
var(--color-surface) 92%
);
border-radius: 0 0 12px 12px;
border: 2px solid var(--color-border);
width: 100%;
box-sizing: border-box;
min-width: 0;
overflow: hidden;
}
.audio-player-btn {
flex-shrink: 0;
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
border: none;
background: var(--color-accent);
color: var(--color-on-accent);
cursor: pointer;
display: grid;
place-items: center;
padding: 0.45rem;
transition: background 0.15s, transform 0.1s;
}
.audio-player-btn:hover {
background: var(--color-accent-hover);
transform: scale(1.08);
}
.audio-player-btn svg {
width: 100%;
height: 100%;
display: block;
}
.audio-player-time {
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
opacity: 0.6;
flex-shrink: 0;
}
.audio-player-track {
position: relative;
flex: 1;
min-width: 48px;
height: 4px;
border-radius: 2px;
background: color-mix(
in srgb,
var(--color-accent) 20%,
var(--color-border) 80%
);
}
/* ── Waveform ── */
.waveform-svg {
flex: 1;
min-width: 0;
height: 48px;
cursor: pointer;
display: block;
}
.waveform-bar {
fill: color-mix(in srgb, var(--color-accent) 25%, var(--color-border) 75%);
}
.waveform-bar--played {
fill: var(--color-accent);
}
.waveform-skeleton {
flex: 1;
min-width: 0;
height: 48px;
border-radius: 3px;
background: color-mix(
in srgb,
var(--color-accent) 12%,
var(--color-border) 88%
);
position: relative;
overflow: hidden;
cursor: pointer;
}
.waveform-skeleton-fill {
position: absolute;
inset: 0 auto 0 0;
background: var(--color-accent);
opacity: 0.45;
transition: width 0.1s linear;
}
.audio-player-track--volume {
flex: 1 1 100px;
max-width: 120px;
}
.audio-player-fill {
position: absolute;
inset: 0 auto 0 0;
background: var(--color-accent);
border-radius: 2px;
pointer-events: none;
}
.audio-player-range {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.audio-player-volume {
display: flex;
align-items: center;
gap: 0.4rem;
flex-shrink: 1;
min-width: 0;
}
.audio-player-vol-btn {
flex-shrink: 0;
width: 1.4rem;
height: 1.4rem;
border: none;
background: transparent;
color: var(--color-text);
cursor: pointer;
display: grid;
place-items: center;
padding: 0;
opacity: 0.55;
transition: opacity 0.15s;
}
.audio-player-vol-btn:hover {
opacity: 1;
}
.audio-player-vol-btn svg {
width: 100%;
height: 100%;
display: block;
}
.file-preview-pdf {
width: 100%;
height: 80vh;
min-height: 600px;
border-radius: 8px;
border: 2px solid var(--color-border);
display: block;
}
.file-download-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: 2px solid var(--color-accent);
border-radius: 8px;
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
transition: background 0.15s, color 0.15s;
}
.file-download-link:hover {
background: var(--color-accent);
color: var(--color-on-accent);
}
.dump-file-notice {
margin: 0 0 0.25rem;
font-size: 0.9rem;
opacity: 0.8;
}
.rich-content-card {
display: flex;
border: 2px solid var(--color-border);
border-radius: 10px;
overflow: hidden;
transition: border-color 0.2s;
}
.rich-content-card:has(.rich-content-body:hover) {
border-color: var(--color-accent);
}
.rich-content-card--youtube {
border-color: var(--color-youtube);
}
.rich-content-card--youtube:has(.rich-content-body:hover) {
border-color: var(--color-youtube-hover);
}
.rich-content-card--youtube .rich-content-badge {
background: var(--color-youtube);
}
.rich-content-card--bandcamp {
border-color: var(--color-bandcamp);
}
.rich-content-card--bandcamp:has(.rich-content-body:hover) {
border-color: var(--color-bandcamp-hover);
}
.rich-content-card--bandcamp .rich-content-badge {
background: var(--color-bandcamp);
}
.rich-content-card--soundcloud {
border-color: var(--color-soundcloud);
}
.rich-content-card--soundcloud:has(.rich-content-body:hover) {
border-color: var(--color-soundcloud-hover);
}
.rich-content-card--soundcloud .rich-content-badge {
background: var(--color-soundcloud);
}
/* Brand badges sit on a saturated brand colour — use the fixed white
foreground rather than the accent-tuned `on-accent` of the generic badge. */
.rich-content-card--youtube .rich-content-badge,
.rich-content-card--bandcamp .rich-content-badge,
.rich-content-card--soundcloud .rich-content-badge {
color: var(--color-on-brand);
}
/* ── Global persistent player ── */
.global-player {
position: fixed;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 2.5rem);
max-width: 860px;
z-index: 1000;
background: var(--color-surface);
border: 2px solid var(--color-border);
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.3);
animation: player-enter 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both;
transition: opacity 0.2s ease, box-shadow 0.2s ease;
}
.global-player--reduced {
opacity: 0.6;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.global-player--reduced:hover {
opacity: 1;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.3);
}
@keyframes player-enter {
from {
transform: translateX(-50%) translateY(1.5rem);
}
to {
transform: translateX(-50%) translateY(0);
}
}
.global-player-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 1rem;
}
.global-player-title {
flex: 1;
font-weight: 600;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
a.global-player-title {
color: inherit;
text-decoration: none;
}
a.global-player-title:hover {
text-decoration: underline;
}
.global-player-body {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.3s ease;
}
.global-player--reduced .global-player-body {
grid-template-rows: 0fr;
}
.global-player-iframe-wrap,
.global-player-media-wrap {
overflow: hidden;
min-height: 0;
border-radius: 0 0 8px 8px;
}
/* Inside the global player the shell already draws the border — drop the
nested media player's own border so it isn't doubled up. */
.global-player .audio-player,
.global-player .video-player {
border: none;
}
.global-player iframe {
width: 100%;
border: none;
display: block;
}
.global-player-iframe--youtube {
aspect-ratio: 16/9;
max-height: 40vh;
}
.global-player-iframe--soundcloud {
height: 166px;
}
.global-player-iframe--bandcamp {
height: 120px;
}
.global-player.global-player--bandcamp {
max-width: 600px;
}
.feed-loading-more {
text-align: center;
padding: 1rem;
color: var(--color-text-muted);
font-size: 0.85rem;
}
body.has-player {
padding-bottom: var(--player-height, 0px);
}
/* ── Floating "new dump" button ───────────────────────────────────────
Revealed once the header scrolls out of view. Bottom-right by default, but
moves to the top-right while the global player is mounted (it owns the
bottom of the screen, full-width on mobile). Sits below the player and
modal backdrop (both z-index 1000). */
.dump-fab {
position: fixed;
--fab-size: 3.5rem;
/* Sit just outside the right edge of the centered content lane — in the
margin beside it — rather than over the content or pinned to the viewport
edge. The lane width is `--fab-lane` (860px for most feeds; the journal
grid is wider, see below). Falls back to a 1.25rem viewport gutter once
that margin is too narrow to hold the button. */
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
/* Anchored to the bottom-right by default, but expressed via `top` (rather
than `bottom`) so the jump to the top-right when a player mounts can
animate as a slide. `dvh` tracks the visible viewport on mobile; `vh` is
the fallback for browsers without it. */
top: calc(100vh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom, 0px));
top: calc(100dvh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom, 0px));
z-index: 900;
width: var(--fab-size);
height: var(--fab-size);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
/* Circle by default; a theme can square it off by setting --fab-radius: 0. */
border-radius: var(--fab-radius, 50%);
background: var(--color-accent);
color: var(--color-on-accent);
cursor: pointer;
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%, transparent),
0 2px 6px rgba(0, 0, 0, 0.3);
/* Hidden state: faded out, nudged down, non-interactive. */
opacity: 0;
transform: translateY(1rem) scale(0.9);
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease,
top 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.dump-fab-icon {
width: 1.6rem;
height: 1.6rem;
}
.dump-fab--visible {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.dump-fab:hover {
background: var(--color-accent-hover);
transform: translateY(-2px) scale(1);
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%, transparent),
0 2px 6px rgba(0, 0, 0, 0.3);
}
.dump-fab:active {
transform: translateY(0) scale(0.96);
}
/* The journal tab lays out its masonry grid in a wider lane than the other
feeds, so the button must track that wider edge to stay aligned with it. */
body:has(.journal-grid) {
--fab-lane: 1180px;
}
/* The player occupies the bottom of the screen (full-width on mobile), so when
one is mounted slide the button up to the top-right corner to stay clear. */
body.has-player .dump-fab {
top: calc(1.25rem + env(safe-area-inset-top, 0px));
}
/* Reserve room at the end of the scroll so the button never hides the last
content when scrolled all the way down. */
body.has-fab .page-content {
padding-bottom: 5rem;
}
/* Chat FAB — mirrors .dump-fab and stacks directly above it (the "+" dump
button keeps the bottom slot; chat sits one button-height up) so the two
never overlap. */
.chat-fab {
position: fixed;
--fab-size: 3.5rem;
/* Share the dump-fab's column. */
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
/* One button-height + gap above the dump-fab's bottom-anchored resting spot. */
top: calc(100vh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom, 0px));
top: calc(100dvh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom, 0px));
z-index: 900;
width: var(--fab-size);
height: var(--fab-size);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
border-radius: var(--fab-radius, 50%);
background: var(--color-accent);
color: var(--color-on-accent);
cursor: pointer;
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%, transparent),
0 2px 6px rgba(0, 0, 0, 0.3);
opacity: 0;
transform: translateY(1rem) scale(0.9);
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease,
top 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.chat-fab-icon {
width: 1.6rem;
height: 1.6rem;
}
.chat-fab--visible {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.chat-fab:hover {
background: var(--color-accent-hover);
transform: translateY(-2px) scale(1);
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%, transparent),
0 2px 6px rgba(0, 0, 0, 0.3);
}
.chat-fab:active {
transform: translateY(0) scale(0.96);
}
/* Match the dump-fab's clearance shift when a player is mounted. */
body.has-player .chat-fab {
top: calc(1.25rem + env(safe-area-inset-top, 0px));
/* Stack below the dump-fab in the top-right corner to avoid overlap. */
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
top: calc(1.25rem + var(--fab-size) + 0.75rem + env(safe-area-inset-top, 0px));
}
/* The badge sits on the circular FAB; nudge it onto the rim. */
.chat-fab .notification-badge {
top: 2px;
right: 2px;
box-shadow: 0 0 0 2px var(--color-accent);
}
.rich-content-thumbnail-btn {
position: relative;
padding: 0;
border: none;
background: none;
cursor: pointer;
display: flex;
align-self: stretch;
overflow: hidden;
flex-shrink: 0;
}
.rich-content-play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.25);
color: #fff;
font-size: 1.75rem;
transition: background 0.18s ease;
}
.rich-content-thumbnail-btn:hover .rich-content-play-overlay {
background: rgba(0, 0, 0, 0.5);
}
.rich-content-thumbnail {
width: 180px;
min-width: 180px;
object-fit: cover;
display: block;
}
@media (max-width: 512px) {
.rich-content-card {
flex-direction: column;
}
.rich-content-thumbnail {
width: 100%;
min-width: 0;
max-height: 180px;
}
}
.rich-content-body {
display: flex;
flex-direction: column;
text-decoration: none;
color: var(--color-text);
gap: 0.4rem;
padding: 0.9rem 1.1rem;
flex: 1;
min-width: 0;
}
/* Generic badge (unknown providers) — themed accent. Brand providers override
the background below and switch to the fixed white `on-brand` foreground. */
.rich-content-badge {
display: inline-block;
background: var(--color-accent);
color: var(--color-on-accent);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 0.15rem 0.5rem;
border-radius: 4px;
align-self: flex-start;
}
.rich-content-title {
margin: 0;
font-weight: 600;
font-size: 1rem;
line-height: 1.35;
overflow-wrap: break-word;
word-break: break-word;
}
/* Multi-line truncation boilerplate. Each owner sets its own
`-webkit-line-clamp` line count; this group carries the rest. */
.rich-content-description,
.dump-card-comment,
.playlist-card-description,
.journal-card-title,
.journal-card--image .journal-card-comment,
.journal-card-quote-body {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.rich-content-description {
margin: 0;
font-size: 0.85rem;
opacity: 0.75;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.rich-content-url {
font-size: 0.75rem;
opacity: 0.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
.dump-card--fading {
filter: brightness(0.65);
}
.dump-card--dismissing {
opacity: 0;
filter: brightness(0.25);
grid-template-rows: 0fr;
pointer-events: none;
}
.rich-content-compact {
display: flex;
align-items: center;
text-decoration: none;
flex-shrink: 0;
}
.rich-content-compact-thumbnail {
width: 36px;
height: 36px;
object-fit: cover;
border-radius: 4px;
border: 1px solid var(--color-border);
display: block;
}
.rich-content-compact-icon {
font-size: 1rem;
opacity: 0.6;
}
.avatar-img {
object-fit: cover;
border: 2px solid var(--color-surface);
border-radius: 50%;
display: block;
}
.avatar-initials {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--color-accent);
color: var(--color-on-accent);
font-weight: 700;
user-select: none;
flex-shrink: 0;
}
/* ── Vote button ── */
.vote-btn {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.6rem;
border: 2px solid var(--color-border);
border-radius: 6px;
background: transparent;
color: var(--color-text);
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s;
white-space: nowrap;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
}
.vote-btn:hover:not(:disabled) {
border-color: var(--color-accent);
color: var(--color-accent);
}
.vote-btn--active {
border-color: var(--color-accent);
background: var(--color-accent);
color: var(--color-on-accent);
}
.vote-btn--active:hover:not(:disabled) {
background: var(--color-accent-hover);
border-color: var(--color-accent-hover);
color: var(--color-on-accent);
}
.vote-btn:focus {
outline: none;
}
.vote-btn:focus-visible {
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-accent) 35%, transparent);
}
.vote-btn:disabled {
opacity: 0.5;
cursor: default;
}
/* Clickable counts. Padding + matching negative margin enlarge the click
target without shifting layout — the box grows, its visual position holds. */
.vote-count-clickable,
.like-count-clickable {
display: inline-block;
cursor: pointer;
}
.vote-count-clickable:hover,
.like-count-clickable:hover {
text-decoration: underline;
}
.vote-count-clickable {
padding: 0.4rem;
margin: -0.4rem;
}
/* ── Dump OP line ── */
.dump-op {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem 0.5rem;
font-size: 0.85rem;
opacity: 0.7;
}
.dump-op-link {
font-weight: 600;
color: var(--color-text);
text-decoration: none;
min-width: 0;
overflow-wrap: anywhere;
}
.dump-op-link:hover {
color: var(--color-accent);
}
/* ── Avatar edit overlay ── */
/* ── ImagePicker (reusable clickable cover image) ── */
.img-picker {
position: relative;
flex-shrink: 0;
cursor: pointer;
outline: none;
}
.img-picker-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border: 1px solid var(--color-border);
}
.img-picker-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed var(--color-border);
background: var(--color-bg);
color: var(--color-text);
font-size: 1.5rem;
opacity: 0.4;
box-sizing: border-box;
}
.img-picker-overlay {
position: absolute;
inset: 0;
background: var(--color-overlay);
color: var(--color-on-accent);
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.15s;
}
.img-picker:hover .img-picker-overlay,
.img-picker:focus-visible .img-picker-overlay {
opacity: 1;
}
.profile-avatar-wrapper {
position: relative;
flex-shrink: 0;
}
.avatar-change-overlay {
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--color-overlay);
color: var(--color-on-accent);
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
cursor: pointer;
transition: opacity 0.15s;
}
.profile-avatar-wrapper:hover .avatar-change-overlay {
opacity: 1;
}
/* ── Public profile page ── */
.profile-header {
display: flex;
align-items: flex-start;
gap: 1.5rem;
}
.profile-info {
position: relative;
min-width: 0;
}
.profile-info-scroll {
overflow-x: auto;
scrollbar-width: none;
padding-bottom: 0.25rem;
padding-right: 0.25rem;
}
.profile-info-scroll::-webkit-scrollbar {
display: none;
}
/* Fade hint only at narrow viewports where the info column can actually overflow */
@media (max-width: 600px) {
.profile-info::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 2rem;
background: linear-gradient(to right, transparent, var(--color-bg));
pointer-events: none;
}
}
.profile-tabs-scroller {
margin-top: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--color-border);
}
.profile-columns {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 900px) {
.profile-columns {
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: start;
}
}
.profile-section ul {
list-style: none;
margin: 0;
padding: 0;
}
.empty-state {
margin: 0;
opacity: 0.5;
font-size: 0.9rem;
}
/* ── Profile section header (shared between bare h2 and wrapper div) ── */
.profile-section > h2,
.profile-section-header {
margin-bottom: 0.75rem;
border-bottom: 2px solid var(--color-border);
padding-bottom: 0.4rem;
}
.profile-section-title {
margin: 0;
}
.profile-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
/* ── Profile (own) page ── */
.profile-username-row {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.profile-username {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
/* Role badge — elevated roles only (see RoleChip) */
.profile-role-chip {
padding: 0.1rem 0.55rem;
border-radius: 999px;
background: var(--color-accent);
color: var(--color-on-accent);
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
.profile-role-chip--moderator {
background: var(--color-success);
}
.profile-header .btn-border {
margin-top: 0.5rem;
}
.profile-header .follow-btn {
margin-top: 0.5rem;
}
/* ── Profile description ── */
.profile-description {
width: 100%;
margin-bottom: 1.5rem;
}
.profile-description-view {
position: relative;
padding: 0.5rem 0.6rem;
border-radius: 6px;
font-size: 0.95rem;
}
.profile-description-view--editable {
cursor: pointer;
}
.profile-description-view--editable:hover {
background: var(--color-surface);
}
.profile-description-text {
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
line-height: 1.6;
}
.profile-description-empty {
color: var(--color-muted);
font-style: italic;
}
.profile-description-edit-btn {
position: absolute;
top: 0.55rem;
right: 0.5rem;
font-size: 0.85rem;
color: var(--color-muted);
opacity: 0;
transition: opacity 0.1s;
pointer-events: none;
}
.profile-description-view--editable:hover .profile-description-edit-btn {
opacity: 1;
}
.profile-description-editor {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.profile-email-display {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.78rem;
color: var(--color-text-muted);
margin: 0.1rem 0 0.4rem;
cursor: pointer;
border-radius: 4px;
padding: 0.1rem 0.2rem;
margin-left: -0.2rem;
}
.profile-email-display:hover {
background: var(--color-surface);
}
.profile-email-display:hover .profile-description-edit-btn {
opacity: 1;
}
.profile-email-editor {
margin: 0.2rem 0 0.4rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.profile-email-input {
width: min(260px, 100%);
padding: 0.35rem 0.6rem;
border-radius: 6px;
font-size: 0.85rem;
line-height: 1.5;
}
.profile-email-actions {
display: flex;
gap: 0.4rem;
}
.profile-email-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 1.75rem;
padding: 0 0.65rem;
border-radius: 5px;
border: 1.5px solid;
font-size: 0.8rem;
font-family: inherit;
font-weight: 500;
cursor: pointer;
box-sizing: border-box;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.profile-email-btn--save {
background: var(--color-accent);
color: var(--color-on-accent);
border-color: var(--color-accent);
}
.profile-email-btn--save:hover:not(:disabled) {
background: var(--color-accent-hover);
border-color: var(--color-accent-hover);
}
.profile-email-btn--save:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.profile-email-btn--cancel {
background: transparent;
color: var(--color-text-muted);
border-color: var(--color-border);
}
.profile-email-btn--cancel:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
.profile-description-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.profile-invited-by {
font-size: 0.78rem;
color: var(--color-text-muted);
margin: 0.15rem 0 0.4rem;
}
.profile-invited-by--founding {
font-style: italic;
}
.profile-invited-by-link {
color: var(--color-text-muted);
text-decoration: none;
font-weight: 600;
}
.profile-invited-by-link:hover {
color: var(--color-accent);
}
.profile-own-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
align-items: center;
}
.profile-own-actions .btn-border {
margin-top: 0;
}
.invite-generate {
display: contents;
}
.invite-btn {
padding: 0.3rem 0.9rem;
border: 1.5px solid var(--color-accent);
border-radius: 6px;
background: transparent;
color: var(--color-accent);
font-size: 0.85rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.invite-btn:hover {
background: var(--color-accent);
color: var(--color-on-accent);
}
.invite-result {
display: flex;
align-items: center;
gap: 0.5rem;
max-width: 480px;
}
.invite-url {
font-size: 0.75rem;
font-family: var(--font-mono, monospace);
color: var(--color-text-muted);
flex: 1;
min-width: 0;
overflow-x: auto;
white-space: nowrap;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
padding: 0.35rem 0.6rem;
}
/* Always-visible copy target. Sits first so it's the obvious tap. */
.invite-copy-btn {
order: -1;
flex-shrink: 0;
padding: 0.2rem 0.65rem;
border: 1px solid var(--color-border-subtle);
border-radius: 4px;
background: transparent;
color: var(--color-text);
font-size: 0.75rem;
cursor: pointer;
white-space: nowrap;
transition: background 0.12s;
}
.invite-copy-btn:hover {
background: var(--color-accent);
color: var(--color-on-accent);
border-color: var(--color-accent);
}
/* ── Profile sub-pages (dumps / upvoted / playlists) ── */
.profile-subpage-back {
display: inline-block;
font-size: 0.85rem;
color: var(--color-text-muted);
text-decoration: none;
margin-bottom: 0.75rem;
}
.profile-subpage-back:hover {
color: var(--color-text);
}
.profile-subpage-title-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.profile-subpage-title {
margin: 0;
font-size: 1.4rem;
font-weight: 700;
flex: 1;
}
/* ── Profile "View all" link ── */
.profile-view-all {
display: inline-block;
margin-top: 0.75rem;
font-size: 0.85rem;
color: var(--color-text-muted);
text-decoration: none;
}
.profile-view-all:hover {
color: var(--color-accent);
}
/* ── Border-tinted button ── */
.btn-border {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.3rem 0.9rem;
border: 1.5px solid var(--color-border);
border-radius: 6px;
background: transparent;
color: var(--color-text-muted);
font-size: 0.85rem;
font-family: inherit;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.btn-border:hover {
border-color: var(--color-danger);
color: var(--color-danger);
}
/* ── ErrorCard ── */
.error-card-wrap {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.error-card {
display: flex;
align-items: flex-start;
gap: 0.6rem;
background: var(--color-danger-bg);
color: var(--color-text);
padding: 0.7rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
}
.error-card-icon {
flex-shrink: 0;
line-height: 1.45;
}
.error-card-body {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.error-card-title {
margin: 0;
font-size: 0.95rem;
font-weight: 700;
}
.error-card-message {
margin: 0;
line-height: 1.45;
opacity: 0.85;
}
.error-card-actions {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.app-header-status {
margin: 1rem auto 0;
max-width: 860px;
width: calc(100% - 2rem);
padding: 0.75rem 1rem;
border: none;
border-radius: 10px;
background: var(--color-danger-bg);
color: var(--color-text);
line-height: 1.5;
overflow-wrap: anywhere;
word-break: break-word;
}
.app-header-status strong {
font-size: 0.95rem;
}
.page-error-wrap {
margin: 2rem auto;
max-width: 480px;
width: 100%;
}
/* ── Shared layout ── */
.page-shell {
display: flex;
flex-direction: column;
}
.page-content {
width: 100%;
max-width: 860px;
margin: 0 auto;
padding: 2rem 1.25rem 0;
box-sizing: border-box;
animation: page-enter 0.2s ease both;
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.page-content--centered {
align-items: center;
padding-top: 2.5rem;
}
.page-loading {
text-align: center;
opacity: 0.6;
padding: 4rem 1rem;
}
/* ── Shared header ── */
.app-header {
position: relative;
display: flex;
align-items: center;
gap: 1rem;
padding: 0 1.25rem;
min-height: 3.75rem;
background: var(--color-surface);
border-bottom: 2px solid var(--color-border);
}
@media (min-width: 860px) {
.app-header--has-center {
display: grid;
grid-template-columns: auto 1fr auto;
}
.app-header--has-center .app-header-nav {
grid-column: 3;
}
}
.app-header-brand {
font-size: 1.15rem;
font-weight: 700;
letter-spacing: -0.01em;
flex-shrink: 0;
color: var(--color-text);
text-decoration: none;
}
.app-header-center {
display: none;
align-items: center;
justify-content: center;
min-width: 0;
overflow-x: clip;
overflow-y: visible;
container-type: inline-size;
}
@media (min-width: 860px) {
.app-header-center {
display: flex;
}
.app-header-center .search-bar-btn {
display: none;
}
}
/* Below-header search row — small viewports only */
.header-search-below {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
background: var(--color-surface);
border-bottom: 2px solid var(--color-border);
}
.header-search-below .search-bar {
flex: 1;
}
.header-search-below .search-bar-input {
max-width: 100%;
}
@media (min-width: 860px) {
.header-search-below {
display: none;
}
}
/* When the search bar is expanded, immediately clear the rest of the center —
the expanded input needs the full column width */
.app-header-center:has(.search-bar--expanded) .index-presence,
.app-header-center:has(.search-bar--expanded) .feed-sort {
display: none;
}
/* As the center column shrinks (viewport narrow, search collapsed),
shed content in order: presence first, then tabs (still in .index-below-header) */
@container (max-width: 460px) {
.index-presence {
display: none;
}
}
@container (max-width: 280px) {
.feed-sort {
display: none;
}
}
.header-center-slot {
display: flex;
align-items: center;
gap: 0.75rem;
}
.app-header-brand:hover {
color: var(--color-accent);
}
.app-header-nav {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.6rem;
margin-left: auto;
flex-shrink: 0;
flex-wrap: nowrap;
}
/* Normalize every header-nav control to one height (search / notifications /
avatar / + new were all different sizes) and centre their contents — flex +
line-height:1 keeps text and emoji glyphs vertically centred in the box. */
.app-header-nav button,
.app-header-user {
height: 2.25rem;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
font-family: inherit;
line-height: 1;
}
.app-header-nav button {
padding: 0.35rem 0.85rem;
}
.app-header-nav .btn-primary {
padding: 0.35rem 1rem;
}
/* Icon-only controls are square, so the row reads as a tidy set. */
.app-header-nav .nav-search-btn,
.app-header-nav .notification-bell,
.app-header-nav .chat-button,
.app-header-nav .user-menu-trigger {
width: 2.25rem;
padding: 0;
justify-content: center;
}
/* Ghost search button — always visible except when the center search bar is shown */
.nav-search-btn {
display: flex;
align-items: center;
padding: 0.35rem;
background: transparent;
border: none;
color: inherit;
text-decoration: none;
font-size: 1rem;
cursor: pointer;
border-radius: 6px;
}
.nav-search-btn:hover {
background: var(--color-surface);
}
/* Text links — visible only at wide viewports */
.nav-links {
display: none;
align-items: center;
gap: 0.6rem;
}
@media (min-width: 1150px) {
.nav-links {
display: flex;
}
}
/* Avatar menu — visible only below the text-links breakpoint */
.nav-compact {
display: flex;
align-items: center;
}
@media (min-width: 1150px) {
.nav-compact {
display: none;
}
}
.app-header-user {
display: inline-flex;
align-items: center;
text-decoration: none;
color: var(--color-text);
font-weight: 600;
font-size: 0.95rem;
padding: 0.35rem 0.85rem;
border-radius: 8px;
background: var(--color-header-user-bg);
transition: background 0.15s;
}
.app-header-user:hover {
background: var(--color-header-user-bg-hover);
}
/* ── User menu (compact nav) ── */
.user-menu {
position: relative;
}
.user-menu-trigger {
display: flex;
align-items: center;
background: transparent;
border: none;
border-radius: 8px;
padding: 0.25rem;
cursor: pointer;
transition: background 0.15s;
}
.user-menu-trigger:hover {
background: var(--color-header-user-bg-hover);
}
.user-menu-dropdown {
position: absolute;
top: calc(100% + 0.4rem);
right: 0;
min-width: 150px;
background: var(--color-surface);
border: 2px solid var(--color-border-subtle);
border-radius: 10px;
padding: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
z-index: 100;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.user-menu-item {
display: block;
padding: 0.45rem 0.75rem;
border-radius: 7px;
text-decoration: none;
color: var(--color-text);
font-size: 0.9rem;
font-weight: 600;
transition: background 0.12s;
}
.user-menu-item:hover {
background: var(--color-header-user-bg-hover);
}
.user-list-popover {
/* position/top/bottom/left/max-height set inline — anchored to the
trigger's viewport rect, flipping above it when there isn't room below */
min-width: 180px;
overflow-y: auto;
background: var(--color-surface);
border: 2px solid var(--color-border-subtle);
border-radius: 10px;
padding: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
z-index: 100;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.user-list-popover-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
border-radius: 7px;
text-decoration: none;
color: var(--color-text);
font-size: 0.85rem;
font-weight: 600;
transition: background 0.12s;
}
.user-list-popover-item:hover {
background: var(--color-header-user-bg-hover);
}
.user-list-popover-status {
margin: 0;
padding: 0.5rem 0.6rem;
font-size: 0.8rem;
color: var(--color-text-muted);
}
.user-list-popover-load-more {
background: none;
border: none;
cursor: pointer;
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--color-accent);
text-align: left;
font-family: inherit;
}
.user-list-popover-load-more:disabled {
opacity: 0.5;
cursor: default;
}
/* ── Auth card ── */
.auth-card {
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
gap: 1.25rem;
background: var(--color-surface);
border: 2px solid var(--color-border);
border-radius: 12px;
padding: 2rem;
}
.auth-card-title {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
text-align: center;
}
.auth-card-footer {
text-align: center;
font-size: 0.9rem;
opacity: 0.7;
margin: 0;
}
.auth-link-btn {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--color-accent);
font-size: inherit;
font-family: inherit;
text-decoration: underline;
text-underline-offset: 2px;
}
.auth-link-btn:hover {
opacity: 0.8;
}
.auth-reset-panel {
border-top: 1px solid var(--color-border-subtle);
padding-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.auth-reset-form {
margin: 0;
}
.auth-reset-sent {
font-size: 0.9rem;
text-align: center;
margin: 0;
opacity: 0.85;
}
/* ── Form pages (DumpCreate / DumpEdit) ── */
@keyframes page-enter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
.form-page {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
gap: 3px;
animation: page-enter 0.2s ease both;
}
@media (min-width: 860px) {
.form-page--two-col {
grid-template-columns: 1fr 1fr;
}
.form-page--two-col .form-page-header {
grid-column: 1 / -1;
}
.form-page--two-col .dump-edit-preview {
border-radius: 0 0 0 12px;
}
/* Higher specificity than the generic `.form-page .form` below so the
form's bottom-left stays square where it meets the preview. */
.form-page.form-page--two-col .form {
border-radius: 0 0 12px 0;
}
}
.form-page-header {
padding: 1.25rem;
background: var(--color-surface);
border-radius: 12px 12px 0 0;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.form-page-eyebrow {
margin: 0;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
opacity: 0.45;
}
.form-page-title {
margin: 0;
font-size: 1.4rem;
font-weight: 700;
word-break: break-word;
}
.dump-edit-preview {
padding: 1.25rem;
background: var(--color-surface);
overflow: hidden;
border-radius: 0 0 12px 12px;
}
.dump-edit-refresh {
margin-top: 0.75rem;
}
.dump-edit-thumbnail-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.form-page .form {
background: var(--color-surface);
border-radius: 0 0 12px 12px;
padding: 1.25rem;
}
.form-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border-subtle);
margin-top: 0.5rem;
gap: 0.75rem;
}
.form-actions-right {
display: flex;
align-items: center;
gap: 0.75rem;
margin-left: auto;
}
/* Subtle text action. Resets the global <button> chrome so it looks the same
whether rendered as a <button> (modal/page forms) or a <Link> (DumpEdit). */
.form-cancel {
background: none;
border: none;
padding: 0;
cursor: pointer;
font-size: 0.9rem;
color: var(--color-text);
opacity: 0.6;
text-decoration: none;
transition: opacity 0.15s;
}
.form-cancel:hover {
opacity: 1;
}
/* ── Buttons ── */
.btn-primary,
.btn-secondary,
.btn-danger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25em;
white-space: nowrap;
cursor: pointer;
border-radius: 8px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
padding: 0.4rem 1rem;
transition:
background 0.15s,
color 0.15s,
border-color 0.15s,
box-shadow 0.15s,
transform 0.1s;
}
.btn-primary {
background: var(--color-accent);
color: var(--color-on-accent);
border: none;
box-shadow: 0 2px 8px
color-mix(in srgb, var(--color-accent) 40%, transparent);
}
.btn-primary:hover {
background: var(--color-accent-hover);
box-shadow: 0 4px 14px
color-mix(in srgb, var(--color-accent) 50%, transparent);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
box-shadow: none;
}
.btn-primary:disabled {
opacity: 0.45;
cursor: not-allowed;
pointer-events: none;
}
.btn-secondary {
background: none;
border: 1px solid var(--color-border);
color: var(--color-text);
}
.btn-secondary:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
.btn-danger {
background: var(--color-danger-bg);
color: var(--color-text);
border: none;
}
.btn-danger:hover {
background: var(--color-danger-hover);
}
/* ── Ghost button — chromeless icon/toggle action (e.g. global player) ── */
.btn--ghost {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
padding: 0.3rem 0.55rem;
border-radius: 6px;
color: var(--color-text);
font-family: inherit;
opacity: 0.7;
transition: opacity 0.15s, background 0.15s;
}
.btn--ghost:hover {
opacity: 1;
background: color-mix(in srgb, var(--color-text) 8%, transparent);
}
/* ── Ghost icon buttons (shared base) ── */
.modal-close-btn,
.playlist-remove-btn,
.playlist-card-delete-btn {
background: none;
border: none;
cursor: pointer;
line-height: 1;
border-radius: 4px;
transition: opacity 0.15s, color 0.15s;
}
/* ── Index page ── */
.index-page {
display: flex;
flex-direction: column;
animation: page-enter 0.2s ease both;
}
.index-presence {
display: flex;
align-items: center;
gap: 0;
}
@media (min-width: 860px) {
.index-page .dump-feed {
margin-top: 1rem;
}
}
.index-presence-avatar {
display: block;
flex-shrink: 0;
margin-left: -10px;
transition: margin-left 0.2s ease, transform 0.2s ease;
position: relative;
}
.index-presence-avatar:first-child {
margin-left: 0;
}
.index-presence:hover .index-presence-avatar {
margin-left: 4px;
}
.index-presence:hover .index-presence-avatar:first-child {
margin-left: 0;
}
.index-presence-avatar:hover {
transform: translateY(-3px) scale(1.12);
z-index: 2;
}
.index-status {
text-align: center;
opacity: 0.6;
padding: 3rem 1rem;
}
/* ── Below-header strip (presence + sort on narrow viewports) ── */
.index-below-header {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 0.5rem;
gap: 0.75rem;
padding: 0.6rem 1.25rem 0.4rem;
max-width: 860px;
width: 100%;
box-sizing: border-box;
align-self: center;
}
@media (min-width: 860px) {
.index-below-header {
display: none;
}
}
/* ── Feed sort scroller + buttons ── */
/* Wrapper: constrains width, clips scroll overflow, and carries the gradient hint */
.feed-sort-scroller {
position: relative;
min-width: 0;
overflow: hidden;
}
/* Right-edge gradient: absolutely positioned over the scroll container,
always at the viewport-right edge regardless of scroll position */
.feed-sort-scroller::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 2rem;
background: linear-gradient(to right, transparent, var(--color-bg));
pointer-events: none;
}
/* In the app header the tabs never overflow — suppress the gradient there */
.app-header .feed-sort-scroller::after {
display: none;
}
/* In the index below-header the scroller should fill the available row width */
.index-below-header .feed-sort-scroller {
flex: 1;
}
.feed-sort {
display: flex;
align-items: center;
gap: 0.4rem;
overflow-x: auto;
scrollbar-width: none;
padding-bottom: 0.3rem;
padding-right: 0.25rem;
min-width: 0;
}
.feed-sort::-webkit-scrollbar {
display: none;
}
.feed-sort-btn {
padding: 0.25rem 0.8rem;
border-radius: 6px;
border: 2px solid var(--color-border);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
transition: border-color 0.15s, background 0.15s, color 0.15s;
white-space: nowrap;
flex-shrink: 0;
}
.feed-sort-btn.active {
border-color: var(--color-accent);
background: var(--color-accent);
color: var(--color-on-accent);
}
/* ── Dump feed ── */
@keyframes card-enter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.index-page .error-card-wrap {
max-width: 860px;
width: 100%;
padding: 1rem 1.25rem 0;
box-sizing: border-box;
align-self: center;
}
.dump-feed {
list-style: none;
margin: 0;
padding: 1rem 1.25rem 0;
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 860px;
width: 100%;
box-sizing: border-box;
align-self: center;
}
.dump-feed > li {
animation: card-enter 0.2s ease both;
}
/* ── Shared card skin (dump-card + playlist-card) ── */
.dump-card,
.playlist-card {
border: 2px solid var(--color-border);
border-radius: 10px;
background: var(--color-surface);
min-width: 0;
}
.dump-card:hover,
.playlist-card:hover {
border-color: var(--color-accent);
}
/* Card-specific layout */
.dump-card {
display: grid;
grid-template-rows: 1fr;
transition:
border-color 0.15s,
grid-template-rows 0.32s ease,
opacity 0.25s ease,
filter 0.3s ease;
}
.playlist-card {
position: relative;
transition: border-color 0.15s;
}
/* ── Shared card inner layout ── */
.dump-card-inner,
.playlist-card-inner {
overflow: hidden;
min-height: 0;
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
min-width: 0;
cursor: pointer;
}
/* Desktop: wrapper is transparent — preview and vote become direct flex items of dump-card-inner */
.dump-card-left {
display: contents;
}
/* With display:contents, all three are siblings in dump-card-inner's flex context.
DOM order is: preview, vote, body — use order to restore: preview, body, vote. */
.dump-card-body {
order: 1;
margin-left: 0.25rem; /* total gap to preview = container gap (0.75) + this = 1rem */
}
.dump-card-vote {
order: 2;
}
@media (max-width: 512px) {
.dump-card-inner,
.playlist-card-inner {
gap: 0.5rem;
padding: 0.6rem 0.75rem;
}
/* Reset flex order and margin — narrow mode uses dump-card-left as a real flex container */
.dump-card-body,
.dump-card-vote {
order: 0;
}
/* Left column: preview + vote stacked with fixed gap, independent of body height */
.dump-card-left {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
flex-shrink: 0;
}
.dump-card-body {
align-self: center;
}
/* Slightly smaller 16:9 thumb on phones so the body keeps enough width. */
.dump-card-preview {
width: 104px;
height: 58px;
}
.dump-card-meta {
flex-direction: column;
gap: 0.1rem;
}
.dump-card-comment {
display: none;
}
.playlist-card-description {
display: none;
}
}
/* ── Shared card preview thumbnail ── */
.dump-card-preview,
.playlist-card-preview {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
overflow: hidden;
background: var(--color-bg);
/* Subtle matte so any unfilled space (icons, off-ratio art) reads as
intentional framing rather than a broken bar. */
box-shadow: inset 0 0 0 1px var(--color-border);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
/* Feed thumbnails are mostly 16:9 (YouTube, og:image) — match that ratio so
object-fit: cover crops almost nothing, and go wider for legibility. */
.dump-card-preview {
width: 128px;
height: 72px;
}
/* Playlist art is square (album covers) — keep it square. */
.playlist-card-preview {
width: 80px;
height: 80px;
}
.dump-card-inner:hover .dump-card-preview,
.playlist-card-inner:hover .playlist-card-preview {
transform: scale(1.08);
box-shadow: inset 0 0 0 1px var(--color-border), 0 4px 12px rgba(0, 0, 0, 0.25);
}
/* ── Shared card preview icon ── */
.dump-card-preview-icon,
.playlist-card-icon {
font-size: 1.8rem;
opacity: 0.7;
line-height: 1;
}
/* Fill the 48×48 preview box and center content for media buttons */
.dump-card-preview .rich-content-thumbnail-btn {
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.dump-card-preview .rich-content-compact {
width: 100%;
height: 100%;
justify-content: center;
}
.dump-card-preview .rich-content-compact-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0;
border: none;
}
/* ── Shared card body ── */
.dump-card-body,
.playlist-card-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.2rem;
margin-left: 0.25rem;
}
/* ── Shared card title link ── */
.dump-card-title,
.playlist-card-title {
font-weight: 600;
font-size: 1rem;
color: var(--color-text);
text-decoration: none;
word-break: break-word;
transition: color 0.15s;
line-height: 1.35;
}
.dump-card-inner:hover .dump-card-title,
.playlist-card-inner:hover .playlist-card-title {
color: var(--color-accent);
}
/* ── Unread dot ── */
.unread-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-accent);
vertical-align: baseline;
position: relative;
top: -0.15em;
margin-right: 0.4em;
flex-shrink: 0;
}
/* ── Shared card description / comment ── */
.dump-card-comment,
.playlist-card-description {
margin: 0;
font-size: 0.85rem;
opacity: 0.65;
word-break: break-word;
line-height: 1.4;
}
.dump-card-comment {
-webkit-line-clamp: 3;
line-clamp: 3;
}
.playlist-card-description {
-webkit-line-clamp: 2;
line-clamp: 3;
}
/* ── Shared card meta row ── */
.dump-card-date,
.playlist-card-meta {
font-size: 0.78rem;
opacity: 0.45;
margin-top: 0.2rem;
}
.dump-card-meta {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-top: 0.2rem;
}
.dump-card-date {
margin-top: 0;
}
.dump-edited-label,
.playlist-edited-label {
font-size: 0.72rem;
opacity: 0.5;
font-style: italic;
}
.playlist-card-meta {
display: flex;
align-items: center;
gap: 0.6rem;
}
@media (max-width: 512px) {
.playlist-card-meta {
flex-direction: column;
align-items: flex-start;
gap: 0.1rem;
}
}
.dump-card-vote {
flex-shrink: 0;
align-self: center;
}
@media (max-width: 512px) {
.index-below-header .index-presence {
display: none;
}
}
@media (max-width: 512px) {
.app-header-brand-name {
display: none;
}
.btn-new-label {
display: none;
}
}
/* ── Playlist card image thumbnail ── */
.playlist-card-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* ── Playlist badge ── */
.playlist-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.45rem;
border-radius: 4px;
background: color-mix(in srgb, var(--color-accent) 20%, transparent);
color: var(--color-accent);
}
.playlist-badge--private {
background: color-mix(in srgb, var(--color-text-muted) 20%, transparent);
color: var(--color-text-muted);
}
/* ── Playlist card dump count ── */
.playlist-card-count {
opacity: 0.7;
}
.playlist-card-owner,
.playlist-detail-owner {
color: inherit;
text-decoration: none;
font-weight: 600;
}
.playlist-card-owner:hover {
color: var(--color-accent);
}
/* ── Playlist card delete button ── */
.playlist-card-delete-btn {
position: absolute;
top: 0.4rem;
right: 0.4rem;
color: var(--color-text);
opacity: 0;
font-size: 0.8rem;
padding: 0.2rem 0.35rem;
}
.playlist-card:hover .playlist-card-delete-btn {
opacity: 0.4;
}
.playlist-card-delete-btn:hover {
opacity: 1 !important;
color: var(--color-danger);
}
/* ── Modal (shared) ── */
.modal-backdrop {
position: fixed;
inset: 0;
background: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
/* Shared modal box surface */
.modal-card,
.confirm-modal {
background: var(--color-surface);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
width: 100%;
}
.modal-card {
max-width: 420px;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.modal-card--wide {
max-width: 600px;
}
.dump-create-success {
margin-bottom: 1rem;
font-size: 0.95rem;
color: var(--color-text-muted);
}
.dump-create-success a {
color: var(--color-accent);
font-weight: 600;
}
.confirm-modal {
max-width: 340px;
padding: 1.5rem 1.25rem 1.25rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-border-subtle);
}
.modal-title {
font-weight: 700;
font-size: 1rem;
}
.modal-close-btn {
color: var(--color-text);
font-size: 1rem;
opacity: 0.6;
padding: 0.25rem;
}
.modal-close-btn:hover {
opacity: 1;
}
.modal-body {
padding: 1rem 1.25rem;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* ── Live chat ── */
.chat {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
height: min(60vh, 480px);
gap: 0.75rem;
}
.chat-messages {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
/* Reserve a gutter for the scrollbar so it never sits on top of the
messages — without this, overlay scrollbars overlap the content. */
scrollbar-gutter: stable;
display: flex;
flex-direction: column;
gap: 0.85rem;
padding-right: 0.5rem;
}
.chat-empty {
margin: auto 0;
text-align: center;
color: var(--color-text-muted);
}
.chat-load-older {
text-align: center;
padding-bottom: 0.25rem;
}
.chat-message {
display: flex;
gap: 0.6rem;
align-items: flex-start;
/* Each message animates once, when its element first mounts — so newly
arriving messages slide in while existing ones stay put (React reuses
keyed DOM nodes). */
animation: chat-msg-in 0.18s ease-out;
}
@keyframes chat-msg-in {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.chat-message {
animation: none;
}
}
.chat-message-body {
flex: 1;
min-width: 0;
}
.chat-message-meta {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.1rem;
}
.chat-message-author {
font-weight: 700;
font-size: 0.85rem;
color: inherit;
text-decoration: none;
}
.chat-message-author:hover {
color: var(--color-accent);
}
.chat-message-time {
font-size: 0.7rem;
color: var(--color-text-muted);
}
.chat-message-text {
font-size: 0.9rem;
}
.chat-message-text.md {
line-height: 1.45;
}
.chat-message-edited {
font-size: 0.72rem;
color: var(--color-text-muted);
opacity: 0.7;
font-style: italic;
}
/* Edit/Delete actions: bare inline icons that follow the author/time in the
meta row, sitting right next to the message they belong to. Faded in on hover
(or focus, for keyboard use) so they stay unobtrusive. */
.chat-message-actions {
margin-left: 0.35rem;
display: inline-flex;
gap: 0.15rem;
opacity: 0;
transition: opacity 0.15s;
}
.chat-message:hover .chat-message-actions,
.chat-message:focus-within .chat-message-actions {
opacity: 1;
}
@media (hover: none) {
/* No hover on touch devices — keep the actions visible. */
.chat-message-actions {
opacity: 1;
}
}
.chat-icon-btn {
background: none;
border: none;
padding: 0 0.1rem;
font-size: 0.8rem;
line-height: 1;
color: var(--color-text-muted);
cursor: pointer;
transition: color 0.15s;
}
.chat-icon-btn:hover {
color: var(--color-text);
}
.chat-icon-btn--delete:hover {
color: var(--color-danger);
}
.chat-edit {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.chat-edit-actions {
display: flex;
justify-content: flex-end;
gap: 0.4rem;
}
/* Compact buttons for the inline edit box — the full-size form buttons are too
bulky for the narrow message column. */
.chat-edit-actions .btn-primary,
.chat-edit-actions .btn-secondary {
font-size: 0.8rem;
padding: 0.25rem 0.7rem;
box-shadow: none;
}
.chat-composer {
display: flex;
gap: 0.5rem;
align-items: flex-end;
flex-shrink: 0;
}
.chat-composer .mention-textarea-wrap {
flex: 1 1 auto;
min-width: 0;
}
/* Reserve room on the input's bottom-right for the char counter. `display:
block` removes the inline-block baseline descender gap below the textarea —
that phantom space was making the wrapper taller than the textarea, so the
bottom-aligned Send button landed a few px low. */
.chat-input {
padding-right: 3.5rem;
display: block;
}
/* Pin the counter inside the textarea instead of letting it add a row below —
that extra height is what knocked the Send button out of alignment. */
.chat-composer .text-editor-count {
position: absolute;
right: 0.55rem;
bottom: 0.4rem;
margin: 0;
}
.chat-send-btn {
flex-shrink: 0;
/* Match the input's resting height so the two line up exactly (both edges),
while the input is free to grow taller as you type. */
height: 2.4rem;
box-sizing: border-box;
}
/* The composer's popovers (mention list, emoji picker) anchor to the input,
which sits at the very bottom of the modal. The modal body must not clip them
— let them escape the body entirely so they're never cropped. The message
list keeps its own scroll, so nothing else relies on the body clipping. */
.modal-card:has(.chat) .modal-body {
overflow: visible;
}
/* Open the mention list upward, into the roomy message area above the input. */
.chat-composer .mention-dropdown {
top: auto;
bottom: 100%;
margin: 0 0 4px;
}
/* Very narrow viewports: stack the input and Send button. */
@media (max-width: 400px) {
.chat-composer {
flex-direction: column;
align-items: stretch;
}
.chat-send-btn {
height: auto;
width: 100%;
}
/* The author/time/edited row gets cramped when squeezed side-by-side here —
stack the metadata instead so each piece stays readable. Keep the stacked
lines tight so they don't balloon the message height. */
.chat-message-meta {
flex-direction: column;
align-items: flex-start;
gap: 0;
line-height: 1.25;
}
/* No longer trailing the time inline; align it with the stacked metadata. */
.chat-message-actions {
margin-left: 0;
}
}
.confirm-modal-message {
margin: 0;
font-size: 0.95rem;
line-height: 1.5;
}
.confirm-modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.6rem;
}
/* ── Membership rows ── */
.playlist-membership-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
overflow-y: auto;
max-height: 40vh;
}
.playlist-membership-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.75rem;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
}
.playlist-membership-row:hover {
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}
.playlist-membership-row--active {
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
}
.playlist-membership-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.playlist-membership-check {
font-size: 1rem;
color: var(--color-accent);
width: 1.2rem;
text-align: center;
flex-shrink: 0;
}
/* ── Inline new-playlist form ── */
.modal-new-item-toggle {
background: none;
border: 1px dashed var(--color-border-subtle);
border-radius: 8px;
color: var(--color-accent);
cursor: pointer;
padding: 0.6rem 1rem;
font-size: 0.9rem;
text-align: left;
width: 100%;
transition: border-color 0.15s, background 0.15s;
}
.modal-new-item-toggle:hover {
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
}
/* ── Playlist detail page ── */
.playlist-detail-header {
background: var(--color-surface);
border-radius: 12px;
padding: 1.25rem;
}
.playlist-detail-header-top {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.playlist-detail-img {
flex-shrink: 0;
width: 72px;
height: 72px;
object-fit: cover;
border-radius: 8px;
border: 1px solid var(--color-border);
}
.playlist-detail-title-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.playlist-detail-title-row .playlist-edit-input {
flex: 1;
min-width: 0;
}
.playlist-detail-title {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
word-break: break-word;
}
.playlist-detail-description {
font-size: 1rem;
margin: 0 0 0.5rem;
opacity: 0.75;
line-height: 1.75;
overflow-wrap: break-word;
word-break: break-word;
}
.playlist-detail-meta {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 0.82rem;
opacity: 0.6;
}
@media (max-width: 512px) {
.playlist-detail-meta {
flex-direction: column;
align-items: flex-start;
gap: 0.1rem;
}
}
.playlist-detail-owner:hover {
color: var(--color-accent);
opacity: 1;
}
.playlist-detail-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.playlist-edit-btn {
background: none;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
cursor: pointer;
color: var(--color-text);
font-size: 0.85rem;
padding: 0.25rem 0.7rem;
opacity: 0.7;
transition: opacity 0.15s, border-color 0.15s, color 0.15s;
}
.playlist-edit-btn:hover,
.playlist-cancel-btn:hover {
opacity: 1;
border-color: var(--color-accent);
color: var(--color-accent);
}
.playlist-edit-input,
.playlist-edit-textarea {
width: 100%;
font-size: 0.95rem;
padding: 0.4rem 0.65rem;
}
.playlist-edit-textarea {
resize: none;
overflow: hidden;
line-height: 1.5;
}
.playlist-edit-input {
font-size: 1.1rem;
font-weight: 700;
}
/* Inline edit form: stack the fields with a little more breathing room than
the tight display layout, and let the action row sit at the bottom. */
.playlist-edit-content {
gap: 0.7rem;
}
.playlist-detail-meta .playlist-edit-toggle {
opacity: 1;
}
/* ── Playlist dump list ── */
.playlist-dump-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.playlist-dump-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.25rem;
transition: background 0.15s;
border-radius: 12px;
}
.playlist-dump-item--drag-over {
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
outline: 2px dashed var(--color-accent);
}
.playlist-dump-item .dump-card {
flex: 1;
min-width: 0;
}
/* ── Playlist dump item action buttons ── */
.playlist-remove-btn {
flex-shrink: 0;
color: var(--color-text);
opacity: 0.4;
font-size: 0.85rem;
padding: 0.25rem 0.35rem;
}
.playlist-remove-btn:hover {
opacity: 1;
color: var(--color-danger);
}
.playlist-cancel-btn {
flex-shrink: 0;
background: none;
border: 1px solid var(--color-border-subtle);
cursor: pointer;
color: var(--color-text);
opacity: 0.7;
font-size: 0.78rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
transition: opacity 0.15s, border-color 0.15s, color 0.15s;
white-space: nowrap;
}
.drag-handle {
cursor: grab;
opacity: 0.35;
font-size: 1.1rem;
flex-shrink: 0;
user-select: none;
padding: 0 0.25rem;
}
.drag-handle:active {
cursor: grabbing;
}
/* ── Add to playlist button (dump detail) ── */
.btn-add-playlist {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.6rem;
border: 2px solid var(--color-border);
border-radius: 8px;
background: none;
cursor: pointer;
color: var(--color-text);
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
transition: border-color 0.15s, color 0.15s;
}
.btn-add-playlist:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
/* ── New playlist toggle (profile page header) ── */
.new-item-toggle {
background: none;
border: none;
cursor: pointer;
color: var(--color-accent);
font-size: 0.85rem;
padding: 0;
transition: opacity 0.15s;
white-space: nowrap;
}
.new-item-toggle:hover {
opacity: 0.75;
}
/* ── Dump card comment count ── */
.dump-card-comment-count {
font-size: 0.72rem;
color: var(--color-text-muted);
}
/* ── Dump card private badge ── */
.dump-card-private-badge {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.1em 0.45em;
border-radius: 4px;
background: color-mix(in srgb, var(--color-text-muted) 18%, transparent);
color: var(--color-text-muted);
}
/* ── Comments / related (shared section chrome) ── */
.comment-section,
.related-section {
margin-top: 2.5rem;
padding-top: 1.75rem;
border-top: 2px solid var(--color-border);
}
.comment-section-title,
.related-section-title {
font-size: 0.8rem;
font-weight: 700;
color: var(--color-text-muted);
margin: 0 0 1.25rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.comment-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.comment-node {
display: flex;
flex-direction: column;
}
.comment-node-inner {
display: flex;
gap: 0.75rem;
padding: 0.75rem 0.85rem;
border-radius: 8px;
background: var(--color-surface);
transition: background 0.12s;
}
.comment-node-inner:hover {
background: color-mix(
in srgb,
var(--color-surface) 80%,
var(--color-accent) 20%
);
}
.comment-avatar {
flex-shrink: 0;
padding-top: 1px;
}
.comment-content {
flex: 1;
min-width: 0;
}
.comment-meta {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-bottom: 0.4rem;
}
.comment-author {
font-weight: 700;
font-size: 0.85rem;
color: var(--color-accent);
text-decoration: none;
}
.comment-author:hover {
text-decoration: underline;
}
.comment-time {
font-size: 0.75rem;
color: var(--color-text-muted);
text-decoration: none;
}
.comment-time:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
@keyframes comment-highlight {
0% {
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
}
100% {
background: transparent;
}
}
.comment-node--highlight {
border-radius: 6px;
animation: comment-highlight 2s ease-out forwards;
}
.comment-edited {
font-size: 0.72rem;
color: var(--color-text-muted);
opacity: 0.7;
font-style: italic;
}
.comment-body {
font-size: 0.9rem;
line-height: 1.65;
color: var(--color-text);
overflow-wrap: break-word;
word-break: break-word;
}
.comment-actions {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.comment-action-btn {
background: none;
border: 1px solid transparent;
border-radius: 4px;
padding: 0.15rem 0.45rem;
font-size: 0.75rem;
color: var(--color-text-muted);
cursor: pointer;
font-family: inherit;
transition: color 0.1s, border-color 0.1s, background 0.1s;
}
.comment-action-btn:hover {
color: var(--color-text);
border-color: var(--color-border-subtle);
background: color-mix(in srgb, var(--color-text) 6%, transparent);
}
.comment-delete-btn:hover,
.comment-action-btn--liked {
color: var(--color-danger);
border-color: color-mix(in srgb, var(--color-danger) 40%, transparent);
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
}
.comment-action-btn--liked:hover {
color: var(--color-danger);
border-color: color-mix(in srgb, var(--color-danger) 60%, transparent);
background: color-mix(in srgb, var(--color-danger) 16%, transparent);
}
.like-count-clickable {
padding: 0.3rem;
margin: -0.3rem;
}
.comment-replies {
padding-left: max(0.4rem, calc(1.25rem - var(--depth, 0) * 0.1rem));
margin-left: max(0.25rem, calc(1.1rem - var(--depth, 0) * 0.09rem));
margin-top: 0.35rem;
border-left: 2px solid
color-mix(in srgb, var(--color-accent) 30%, transparent);
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.comment-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.6rem;
}
.comment-top-form {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--color-surface);
border-radius: 8px;
border: 1px solid var(--color-border-subtle);
}
.comment-top-form-inner {
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
.comment-top-form-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
}
.comment-reply-textarea {
width: 100%;
box-sizing: border-box;
background: var(--color-bg);
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
padding: 0.55rem 0.75rem;
font-family: inherit;
font-size: 0.875rem;
color: var(--color-text);
resize: none;
overflow: hidden;
min-height: 2.4rem;
transition: border-color 0.15s, box-shadow 0.15s;
}
.comment-reply-textarea:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-accent) 20%, transparent);
}
.comment-form-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.comment-submit-btn {
background: var(--color-accent);
color: #fff;
border: none;
border-radius: 6px;
padding: 0.4rem 1rem;
font-family: inherit;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
letter-spacing: 0.02em;
}
.comment-submit-btn:not(:disabled):hover {
background: var(--color-accent-hover);
}
.comment-submit-btn:disabled {
opacity: 0.4;
cursor: default;
}
.comment-cancel-btn {
background: transparent;
border: 1.5px solid var(--color-border-subtle);
border-radius: 6px;
padding: 0.4rem 1rem;
font-family: inherit;
font-size: 0.82rem;
color: var(--color-text);
opacity: 0.6;
cursor: pointer;
transition: opacity 0.15s;
}
.comment-cancel-btn:hover {
opacity: 1;
}
.comment-node-inner--deleted {
opacity: 0.35;
}
.comment-node-inner--deleted:hover {
background: var(--color-surface);
}
.comment-avatar-placeholder {
border-radius: 50%;
background: var(--color-border-subtle);
}
.comment-deleted-placeholder {
font-size: 0.85rem;
font-style: italic;
color: var(--color-text-muted);
margin: 0;
padding: 0.2rem 0;
}
/* ── Followed user list ── */
.followed-user-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.followed-user-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.4rem 0.5rem;
border-radius: 6px;
}
.followed-user-card-link {
display: flex;
align-items: center;
gap: 0.6rem;
text-decoration: none;
color: var(--color-text);
min-width: 0;
flex: 1;
}
.followed-user-card-link:hover .followed-user-card-name {
color: var(--color-accent);
}
.followed-user-card-name {
font-size: 0.9rem;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Invite tree ── */
.invite-tree {
list-style: none;
padding: 0;
margin: 0;
}
.invite-tree-node {
position: relative;
padding: 0.2rem 0;
}
.invite-tree-node:last-child {
padding-bottom: 0;
}
/* Child list: indented so connector lines have room on the left.
The vertical line sits at the list's left:0, centered under the parent
avatar (avatar center 12px - 1px half-line = 11px). */
.invite-tree-node > .invite-tree {
margin-left: 11px;
padding-left: calc(12px + 0.5rem);
}
/* Connectors are owned per child node (not by the list) so they always anchor
to that child's own avatar center, regardless of how deep its subtree is.
--avatar-center = node top padding (0.2rem) + half the 24px avatar (12px). */
.invite-tree-node > .invite-tree > .invite-tree-node {
--avatar-center: calc(0.2rem + 12px);
}
/* Vertical segment: drops from the top of the child node to its avatar center,
joining the line coming down from the parent / previous sibling. */
.invite-tree-node > .invite-tree > .invite-tree-node::before {
content: "";
position: absolute;
left: calc(-12px - 0.5rem);
top: 0;
height: var(--avatar-center);
width: 2px;
background: var(--color-border-subtle);
}
/* Non-last children extend the vertical segment through their full height
(including any subtree) so it reaches the next sibling. */
.invite-tree-node > .invite-tree > .invite-tree-node:not(:last-child)::before {
height: auto;
bottom: 0;
}
/* Horizontal connector: from the vertical line to the child's avatar. */
.invite-tree-node > .invite-tree > .invite-tree-node::after {
content: "";
position: absolute;
left: calc(-12px - 0.5rem);
top: calc(var(--avatar-center) - 1px); /* straddle center with the 2px bar */
width: calc(12px + 0.5rem);
height: 2px;
background: var(--color-border-subtle);
}
.invite-tree-user {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--color-text);
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
}
.invite-tree-user:hover {
color: var(--color-accent);
}
.invite-tree-empty {
color: var(--color-text-muted);
font-size: 0.9rem;
}
/* ── Follow button ── */
.follow-btn {
padding: 0.25rem 0.9rem;
border-radius: 6px;
border: 2px solid var(--color-accent);
background: transparent;
color: var(--color-accent);
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.follow-btn:hover,
.follow-btn--following {
background: var(--color-accent);
color: var(--color-on-accent, #fff);
}
.follow-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.follow-btn--following:hover {
background: transparent;
color: var(--color-accent);
}
/* ── Followed feed layout ── */
.followed-feed {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.followed-sub-nav {
padding: 0.75rem 1.25rem;
align-self: center;
}
.followed-feed .dump-feed {
padding-top: 0;
}
.followed-feed .index-status {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
/* ── Journal masonry grid ── */
/* ══ Journal — editorial mosaic ══════════════════════════════════════
A composed scrapbook: one hero feature plus an asymmetric mix of
banners, portraits, squares and pull-quotes. Footprints are set by the
layout composer; how a card paints (image / quote / text) follows its
content. Dense auto-flow backfills the gaps the varied spans leave. */
.journal-grid {
list-style: none;
margin: 0 auto;
padding: 1.25rem;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 9rem;
grid-auto-flow: dense;
gap: 0.75rem;
max-width: 1180px;
width: 100%;
box-sizing: border-box;
}
/* ── Footprints ── */
.journal-card--feature {
grid-column: span 2;
grid-row: span 2;
}
.journal-card--wide {
grid-column: span 2;
}
.journal-card--tall {
grid-row: span 2;
}
/* ── Shared shell ── */
.journal-card {
position: relative;
display: flex;
flex-direction: column;
border: 2px solid var(--color-border);
border-radius: 12px;
background: var(--color-surface);
overflow: hidden;
cursor: pointer;
min-width: 0;
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.journal-card:hover {
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
}
.journal-card-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--color-text);
text-decoration: none;
line-height: 1.3;
word-break: break-word;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.journal-card-title:hover {
color: var(--color-accent);
}
.journal-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.journal-card-meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.74rem;
color: var(--color-text-secondary);
flex-wrap: wrap;
min-width: 0;
}
.journal-card-vote {
flex-shrink: 0;
}
/* ── Image cards — full-bleed thumbnail, text laid over a scrim ── */
.journal-card--image .journal-card-image {
position: absolute;
inset: 0;
background: var(--color-bg);
}
.journal-card--image .journal-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* The play affordance is decorative — clicks fall through to the card, which
plays the embed. This keeps the hit target from being covered by the text
overlay on smaller card sizes. */
.journal-card--image .rich-content-play-overlay {
pointer-events: none;
}
.journal-card--image:hover .rich-content-play-overlay {
background: rgba(0, 0, 0, 0.5);
}
.journal-card--image .journal-card-overlay {
position: relative;
margin-top: auto;
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: 1.4rem 0.8rem 0.65rem;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.85) 0%,
rgba(0, 0, 0, 0.55) 55%,
rgba(0, 0, 0, 0) 100%
);
}
.journal-card--image .journal-card-title {
color: #fff;
}
.journal-card--image .journal-card-title:hover {
color: var(--color-accent);
}
.journal-card--image .journal-card-meta {
color: rgba(255, 255, 255, 0.78);
}
.journal-card--image .journal-card-comment {
font-size: 0.82rem;
line-height: 1.4;
margin: 0;
color: rgba(255, 255, 255, 0.85);
-webkit-line-clamp: 2;
line-clamp: 2;
}
.journal-card--feature .journal-card-title {
font-size: 1.25rem;
-webkit-line-clamp: 3;
line-clamp: 3;
}
.journal-card--square.journal-card--image .journal-card-title {
font-size: 0.85rem;
}
/* ── Quote cards — the note is the subject, on an accent wash ── */
.journal-card--quote {
padding: 0.9rem;
gap: 0.4rem;
background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
}
.journal-card-quotemark {
position: absolute;
top: -0.5rem;
right: 0.4rem;
font-family: var(--font-display, Georgia, "Times New Roman", serif);
font-size: 4.5rem;
line-height: 1;
color: var(--color-accent);
opacity: 0.22;
pointer-events: none;
}
.journal-card-quote-body {
position: relative;
z-index: 1;
margin: 0;
font-size: 0.92rem;
line-height: 1.45;
color: var(--color-text);
-webkit-line-clamp: 3;
line-clamp: 3;
}
.journal-card--wide.journal-card--quote .journal-card-quote-body,
.journal-card--feature.journal-card--quote .journal-card-quote-body {
font-size: 1rem;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.journal-card-attribution {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 0.3rem;
padding-top: 0.5rem;
}
.journal-card--quote .journal-card-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--color-text-secondary);
-webkit-line-clamp: 1;
line-clamp: 1;
}
.journal-card--quote .journal-card-title::before {
content: "— ";
}
/* ── Text cards — typographic fallback for note-less, image-less dumps ── */
.journal-card--text {
padding: 0.85rem;
gap: 0.4rem;
}
.journal-card-glyph {
font-size: 1.5rem;
line-height: 1;
}
.journal-card--text .journal-card-title {
font-size: 0.92rem;
-webkit-line-clamp: 3;
line-clamp: 3;
}
.journal-card--text .journal-card-footer {
margin-top: auto;
}
/* feed-end message */
.feed-end {
text-align: center;
padding: 1.5rem 1rem;
font-size: 0.82rem;
color: var(--color-text-secondary);
opacity: 0.55;
}
@media (max-width: 980px) {
.journal-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 680px) {
.journal-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 460px) {
.journal-grid {
grid-template-columns: 1fr;
grid-auto-rows: auto;
padding: 0.85rem 1rem;
}
/* Collapse the mosaic into a single readable column. */
.journal-card {
grid-column: auto;
grid-row: auto;
}
.journal-card--image {
aspect-ratio: 16 / 10;
}
.journal-card--feature.journal-card--image {
aspect-ratio: 4 / 3;
}
}
/* ── Notification bell ── */
@keyframes bell-ring {
0% {
transform: rotate(0deg);
}
12% {
transform: rotate(-20deg);
}
28% {
transform: rotate(20deg);
}
42% {
transform: rotate(-14deg);
}
56% {
transform: rotate(10deg);
}
70% {
transform: rotate(-6deg);
}
84% {
transform: rotate(3deg);
}
100% {
transform: rotate(0deg);
}
}
.notification-bell {
position: relative;
background: var(--color-header-user-bg);
border: none;
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
padding: 0.35rem 0.85rem;
border-radius: 8px;
transition: background 0.15s;
display: inline-flex;
align-items: center;
}
.notification-bell:hover {
background: var(--color-header-user-bg-hover);
}
.notification-bell-icon {
display: inline-block;
transform-origin: 50% 10%;
}
.notification-bell--ringing .notification-bell-icon {
animation: bell-ring 0.65s cubic-bezier(0.36, 0.07, 0.19, 0.97);
}
.notification-badge {
position: absolute;
top: -3px;
right: -3px;
background: var(--color-danger);
color: #fff;
font-size: 0.6rem;
font-weight: 700;
min-width: 16px;
height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
line-height: 1;
pointer-events: none;
box-shadow: 0 0 0 2px var(--color-bg);
}
/* ── Header chat button ── */
.chat-button {
position: relative;
background: var(--color-header-user-bg);
border: none;
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
padding: 0.35rem 0.85rem;
border-radius: 8px;
transition: background 0.15s;
display: inline-flex;
align-items: center;
}
.chat-button:hover {
background: var(--color-header-user-bg-hover);
}
.chat-button-icon {
display: inline-block;
}
/* ── Notifications page ── */
.notifications-page {
max-width: 680px;
margin: 0 auto;
padding: 1.5rem 1rem 3rem;
}
.notifications-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.75rem;
}
.notifications-title {
font-size: 1.4rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.notifications-title-bell {
font-size: 1.2rem;
}
.notifications-unread-pill {
font-size: 0.75rem;
font-weight: 700;
background: color-mix(in srgb, var(--color-danger) 18%, transparent);
color: var(--color-danger);
border: 1px solid color-mix(in srgb, var(--color-danger) 35%, transparent);
border-radius: 12px;
padding: 0.2rem 0.65rem;
white-space: nowrap;
}
.notifications-empty {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-muted);
}
.notifications-empty-icon {
font-size: 2.5rem;
display: block;
margin-bottom: 0.75rem;
opacity: 0.5;
}
.notifications-empty p {
margin: 0.25rem 0;
}
.notifications-empty-hint {
font-size: 0.85rem;
max-width: 340px;
margin: 0.5rem auto 0 !important;
line-height: 1.5;
}
.notif-group {
margin-bottom: 2rem;
}
.notif-group-label {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-muted);
margin: 0 0 0.6rem;
display: flex;
align-items: center;
gap: 0.6rem;
}
.notif-group-label::after {
content: "";
flex: 1;
height: 1px;
background: var(--color-border-subtle);
}
.notification-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.notification-item {
background: var(--color-surface);
border-radius: 10px;
border: 1px solid var(--color-border-subtle);
border-left: 3px solid transparent;
transition: background 0.12s, border-color 0.12s;
overflow: hidden;
}
.notification-item:hover {
background: color-mix(
in srgb,
var(--color-surface) 80%,
var(--color-text) 8%
);
}
.notification-item-link {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.875rem 1rem;
text-decoration: none;
color: inherit;
width: 100%;
}
.notification-item--unread {
border-left-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 9%, var(--color-surface));
}
.notification-item--unread:hover {
background: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface));
}
.notif-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-accent);
box-shadow: 0 0 5px color-mix(in srgb, var(--color-accent) 70%, transparent);
flex-shrink: 0;
align-self: center;
}
.notif-icon {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
flex-shrink: 0;
}
.notif-icon--upvote {
background: color-mix(in srgb, #f59e0b 30%, transparent);
color: #f59e0b;
}
.notif-icon--follow {
background: color-mix(in srgb, #8b5cf6 30%, transparent);
color: #8b5cf6;
}
.notif-icon--dump {
background: color-mix(in srgb, #3b82f6 30%, transparent);
color: #3b82f6;
}
.notif-icon--playlist {
background: color-mix(in srgb, #10b981 30%, transparent);
color: #10b981;
}
.notification-body {
flex: 1;
display: flex;
align-items: baseline;
gap: 0.75rem;
min-width: 0;
}
.notification-content {
flex: 1;
font-size: 0.875rem;
line-height: 1.5;
min-width: 0;
}
.notification-time {
font-size: 0.72rem;
color: var(--color-text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.load-more-btn {
display: block;
margin: 1.5rem auto 0;
padding: 0.5rem 1.5rem;
background: none;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
color: var(--color-text-muted);
transition: border-color 0.15s, color 0.15s;
}
.load-more-btn:hover:not(:disabled) {
border-color: var(--color-accent);
color: var(--color-accent);
}
.load-more-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ── Mention autocomplete ── */
.mention-textarea-wrap {
position: relative;
}
.mention-textarea-wrap textarea {
width: 100%;
box-sizing: border-box;
}
.mention-textarea-wrap--dragover textarea {
outline: 2px dashed var(--color-accent, #6c8ebf);
outline-offset: -2px;
}
.input-with-count {
display: flex;
flex-direction: column;
width: 100%;
}
.text-editor-count {
display: block;
text-align: right;
font-size: 0.7rem;
color: var(--color-text-muted);
margin-top: 2px;
user-select: none;
}
.text-editor-count--warn {
color: var(--color-warning, #c97a00);
}
.text-editor-count--danger {
color: var(--color-danger, #c0392b);
font-weight: 600;
}
.mention-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 200;
list-style: none;
margin: 2px 0 0;
padding: 4px 0;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
max-height: 220px;
overflow-y: auto;
}
.mention-dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
cursor: pointer;
transition: background 0.1s;
}
.mention-dropdown-item:hover,
.mention-dropdown-item--selected {
background: var(--color-bg);
}
.mention-dropdown-username {
font-size: 0.9rem;
color: var(--color-text);
}
/* ── Emoji picker (frimousse) ── */
.emoji-picker-float {
position: absolute;
bottom: calc(100% + 4px);
left: 0;
z-index: 201;
width: 320px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.emoji-picker-search-row {
display: flex;
align-items: center;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg);
}
.emoji-picker-float input {
display: block;
flex: 1;
min-width: 0;
padding: 8px 10px;
border: none;
background: transparent;
color: var(--color-text);
font-size: 0.9rem;
outline: none;
}
.emoji-picker-float input::placeholder {
color: var(--color-text-muted, #888);
}
.emoji-picker-close-btn {
flex-shrink: 0;
padding: 0 10px;
height: 100%;
background: none;
border: none;
color: var(--color-text);
opacity: 0.45;
cursor: pointer;
font-size: 0.75rem;
line-height: 1;
}
.emoji-picker-close-btn:hover {
opacity: 1;
}
/* frimousse uses bare attributes (no data- prefix) */
.emoji-picker-float [frimousse-viewport] {
max-height: 220px;
/* frimousse already sets overflow-y: auto inline */
}
.emoji-picker-float [frimousse-category-header] {
padding: 4px 8px 2px;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-muted, #888);
background: var(--color-surface);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.emoji-picker-float [frimousse-emoji] {
flex: 0 0 calc(100% / var(--frimousse-list-columns, 9));
background: none;
border: none;
padding: 4px 0;
font-size: 1.35em;
line-height: 1;
cursor: pointer;
border-radius: 4px;
transition: background 0.1s;
}
.emoji-picker-float [frimousse-emoji]:hover,
.emoji-picker-float [frimousse-emoji][data-active] {
background: var(--color-bg);
}
.emoji-picker-float [frimousse-loading],
.emoji-picker-float [frimousse-empty] {
display: block;
padding: 16px;
text-align: center;
font-size: 0.85rem;
color: var(--color-text-muted, #888);
}
.notif-icon--mention {
font-weight: 700;
font-family: var(--font-mono, monospace);
}
/* ── Tooltip ── */
.tooltip {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border-subtle);
padding: 0.3em 0.65em;
border-radius: 5px;
font-size: 0.78rem;
font-style: normal;
white-space: nowrap;
pointer-events: none;
z-index: 9999;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
/* ── Search bar (in header center slot) ── */
.search-bar {
display: flex;
align-items: center;
gap: 0.4rem;
width: 100%;
max-width: 420px;
}
.search-bar--collapsible {
width: auto;
max-width: none;
}
.search-bar--collapsible.search-bar--expanded {
width: 100%;
max-width: 420px;
}
.search-bar-input {
flex: 1;
padding: 0.3rem 0.7rem;
border-radius: 8px;
border: 2px solid var(--color-border-subtle);
background: var(--color-bg);
color: var(--color-text);
font-size: 0.875rem;
font-family: inherit;
outline: none;
transition:
max-width 0.25s ease,
opacity 0.2s ease,
padding 0.25s ease,
border-width 0.25s ease,
border-color 0.15s;
min-width: 0;
}
/* Collapsed state: hide the input while keeping it in the DOM */
.search-bar--collapsible .search-bar-input {
max-width: 0;
opacity: 0;
padding-left: 0;
padding-right: 0;
border-width: 0;
pointer-events: none;
}
/* Expanded state: animate it open */
.search-bar--collapsible.search-bar--expanded .search-bar-input {
max-width: 380px;
opacity: 1;
padding-left: 0.7rem;
padding-right: 0.7rem;
border-width: 2px;
pointer-events: auto;
}
.search-bar-input:focus {
border-color: var(--color-accent);
}
.search-bar-btn {
padding: 0.3rem 0.55rem;
background: transparent;
border: 2px solid var(--color-border-subtle);
border-radius: 8px;
cursor: pointer;
font-size: 0.875rem;
flex-shrink: 0;
transition: border-color 0.15s;
}
.search-bar-btn:hover {
border-color: var(--color-accent);
}
/* ── Search page ── */
.search-page {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 3rem;
}
.search-tabs {
width: 100%;
max-width: 860px;
padding: 1rem 1.25rem 0;
box-sizing: border-box;
}
.search-page-empty {
padding: 3rem 1.25rem;
color: var(--color-text-secondary);
text-align: center;
font-size: 0.95rem;
}
/* ── User results ── */
.user-results {
list-style: none;
margin: 0;
padding: 0.75rem 1.25rem 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 860px;
width: 100%;
box-sizing: border-box;
}
.user-result-item {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding: 0.75rem 1rem;
background: var(--color-surface);
border-radius: 10px;
border: 2px solid var(--color-border-subtle);
text-decoration: none;
color: var(--color-text);
transition: border-color 0.15s;
}
.user-result-item:hover {
border-color: var(--color-accent);
}
.user-result-name {
font-weight: 600;
font-size: 0.95rem;
}
.user-result-description {
font-size: 0.85rem;
color: var(--color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Profile appearance settings ── */
.profile-appearance-grid {
display: grid;
grid-template-columns: max-content max-content;
gap: 0.75rem 1rem;
align-items: center;
}
.profile-appearance-row {
display: contents;
}
@media (max-width: 512px) {
.profile-appearance-grid {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.profile-appearance-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.3rem;
}
}
.profile-appearance-label {
font-size: 0.85rem;
font-weight: 600;
opacity: 0.7;
}
/* ── Categories ─────────────────────────────────────────────────────────── */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Scope switcher — the leading item of the feed-sort tab row. The trigger
reuses .feed-sort-btn so it shares the tabs' exact pill metrics; a thin
divider sets it apart from the sort tabs that follow. */
.category-switcher {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.4rem;
flex-shrink: 0;
}
.category-switcher-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.category-switcher-btn:hover,
.category-switcher-btn--open {
border-color: var(--color-accent);
color: var(--color-text);
}
.category-switcher-label {
max-width: 10rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Caret tracks the open state. */
.category-switcher-caret {
width: 0;
height: 0;
border-left: 0.26rem solid transparent;
border-right: 0.26rem solid transparent;
border-top: 0.3rem solid currentColor;
transition: transform 0.15s ease;
}
.category-switcher-btn--open .category-switcher-caret {
transform: rotate(180deg);
}
/* Vertical rule separating the scope selector from the sort tabs. */
.feed-sort-sep {
width: 2px;
height: 1.05rem;
background: var(--color-border);
border-radius: 1px;
flex-shrink: 0;
}
/* Themed popover (fixed-positioned so the row's overflow can't clip it). */
.category-switcher-menu {
position: fixed;
z-index: 200;
min-width: 11rem;
max-height: 60vh;
overflow-y: auto;
margin: 0;
padding: 0.35rem;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.15rem;
background: var(--color-surface);
border: 2px solid var(--color-border-subtle);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.category-switcher-option {
display: block;
width: 100%;
text-align: left;
padding: 0.45rem 0.75rem;
border: none;
border-radius: 7px;
background: transparent;
color: var(--color-text);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
transition: background 0.12s;
}
.category-switcher-option:hover {
background: var(--color-header-user-bg-hover);
}
.category-switcher-option--active {
background: var(--color-accent);
color: var(--color-on-accent);
}
/* Toggle chips used in the dump create/edit forms */
.category-select {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.category-chip-toggle {
padding: 0.25rem 0.7rem;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-bg);
color: var(--color-text-secondary);
font-size: 0.85rem;
cursor: pointer;
}
.category-chip-toggle:hover {
border-color: var(--color-accent);
}
.category-chip-toggle--active {
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-on-accent);
}
/* Read-only category chips on dump cards */
.dump-card-categories {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
margin-top: 0.35rem;
}
.dump-card-category-chip {
padding: 0.1rem 0.5rem;
border-radius: 999px;
background: var(--color-header-user-bg);
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.75rem;
}
.dump-card-category-chip:hover {
background: var(--color-accent);
color: var(--color-on-accent);
}
/* Admin category management — chips that expand into an inline editor */
.category-chips {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.5rem;
}
.category-chip {
display: inline-flex;
align-items: center;
padding: 0.35rem 0.85rem;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-bg);
color: var(--color-text);
font-size: 0.9rem;
font-family: inherit;
line-height: 1.4;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.1s;
}
.category-chip:hover:not(:disabled) {
border-color: var(--color-accent);
color: var(--color-accent);
}
.category-chip:active:not(:disabled) {
transform: scale(0.96);
}
.category-chip:disabled {
opacity: 0.4;
cursor: default;
}
/* Inherits the base chip's font-size/padding so its height matches exactly;
only the glyph weight/color set it apart. */
.category-chip--add {
font-weight: 700;
color: var(--color-text-secondary);
}
.category-chip--add:hover:not(:disabled) {
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-on-accent);
}
/* Inline editor: the expanded form of a chip */
.category-editor-wrap {
display: flex;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.category-editor {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2rem 0.3rem 0.2rem 0.7rem;
border: 1px solid var(--color-accent);
border-radius: 999px;
background: var(--color-bg);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-accent) 16%, transparent);
transform-origin: left center;
animation: category-editor-in 0.18s ease-out;
}
@keyframes category-editor-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.category-editor-input {
min-width: 0;
border: none;
background: transparent;
color: var(--color-text);
font-size: 0.9rem;
font-family: inherit;
padding: 0.2rem 0;
outline: none;
}
.category-editor-input--name {
width: 8.5rem;
font-weight: 600;
}
.category-editor-input--slug {
width: 6.5rem;
font-family: var(--font-mono, monospace);
color: var(--color-text-secondary);
}
.category-editor-sep {
color: var(--color-text-secondary);
opacity: 0.5;
}
.category-editor-actions {
display: inline-flex;
align-items: center;
gap: 0.1rem;
}
.category-editor-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--color-text-secondary);
font-size: 0.8rem;
line-height: 1;
cursor: pointer;
transition: background 0.15s, color 0.15s, filter 0.15s;
}
.category-editor-btn:hover:not(:disabled) {
background: var(--color-header-user-bg);
}
.category-editor-btn:disabled {
opacity: 0.4;
cursor: default;
}
.category-editor-btn--save {
background: var(--color-accent);
color: var(--color-on-accent);
}
.category-editor-btn--save:hover:not(:disabled) {
filter: brightness(1.1);
}
.category-editor-btn--save:disabled {
background: var(--color-border);
color: var(--color-text-secondary);
opacity: 1;
}
.category-editor-btn--delete:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-danger, #c0392b) 18%, transparent);
color: var(--color-danger, #c0392b);
}