Files
gerbeur/src/App.css

6752 lines
145 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: var(--radius-xs);
padding: 0.1em 0.35em;
font-size: 0.88em;
}
.md pre {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 0.75rem 1rem;
overflow-x: auto;
scrollbar-width: thin;
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;
}
/* ── Panels ──
Large surface blocks that have no border of their own. Themes that want
them outlined set --panel-ring (an inset shadow, so blocks that sit flush
against each other keep their spacing). */
.dump-post-header,
.dump-rich-content,
.form-page-header,
.dump-edit-panel,
.dump-edit-footer,
.playlist-detail-header,
.comment-node-inner {
box-shadow: var(--panel-ring), var(--card-shadow);
}
/* ── 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: var(--radius-xl) var(--radius-xl) 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: var(--text-2xl);
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: var(--radius-md);
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: var(--text-sm);
line-height: 1;
flex-shrink: 0;
color: var(--color-text-muted);
opacity: 0;
transition: opacity var(--dur-fast);
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: var(--border-width) solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-surface-sunken);
color: var(--color-text);
font-family: inherit;
outline: none;
transition: border-color var(--dur-slow);
}
.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: var(--text-xl);
font-weight: 700;
}
.dump-title-error {
width: 100%;
margin: 0;
font-size: var(--text-sm);
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: var(--text-base);
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 var(--radius-xl) var(--radius-xl);
}
.dump-url-link {
display: inline-block;
word-break: break-all;
padding: 1rem;
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-lg);
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: var(--text-md);
opacity: 0.7;
transition: opacity var(--dur-base), color var(--dur-base);
}
.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: var(--text-sm);
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: var(--text-xs);
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: var(--radius-lg);
border: var(--border-width) solid var(--color-border);
background-color: var(--color-surface-sunken);
color: var(--color-text);
font-size: var(--text-base);
font-family: var(--font-body);
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
line-height: 1.5;
transition: border-color var(--dur-slow), box-shadow var(--dur-slow), background-color var(--dur-slow);
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: var(--input-focus-bg);
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: var(--text-xs);
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: var(--text-base);
line-height: 1;
color: var(--color-muted);
border-radius: var(--radius-sm);
padding: 0.4rem;
transition: color var(--dur-base);
}
.dump-create-title-edit-btn:hover {
color: var(--color-text);
}
.dump-create-title {
margin: 0;
font-size: var(--text-2xl);
font-weight: 700;
}
/* ── Create-dump modal ── */
/* A file dropped anywhere in the modal is accepted, so the whole body lights up. */
.dump-create--drag {
outline: 2px dashed var(--color-accent);
outline-offset: 6px;
border-radius: var(--radius-xl);
}
.dump-create-draft,
.dump-create-confirm {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 0.9rem;
padding: 0.55rem 0.75rem;
border-radius: var(--radius-lg);
font-size: var(--text-sm);
}
.dump-create-draft {
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
color: var(--color-text-secondary);
}
.dump-create-draft-discard {
background: none;
border: none;
padding: 0;
cursor: pointer;
font: inherit;
font-weight: 600;
color: var(--color-link);
text-decoration: underline;
}
.dump-create-confirm {
background: var(--color-danger-bg);
border: 1px solid var(--color-danger);
}
.dump-create-confirm p {
margin: 0;
flex: 1 1 14rem;
}
.dump-create-confirm-actions {
display: flex;
gap: 0.5rem;
}
/* Not reached, or reached with nothing worth showing. */
.preview-note {
margin: 0;
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
/* ── "Already dumped" hint ── */
.dump-duplicate {
display: flex;
gap: 0.6rem;
padding: 0.65rem 0.8rem;
border-radius: var(--radius-lg);
background: color-mix(in srgb, var(--color-accent) 9%, transparent);
border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
}
.dump-duplicate-icon {
font-size: var(--text-base);
line-height: 1.3;
opacity: 0.8;
}
.dump-duplicate-body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.dump-duplicate-lead {
margin: 0;
font-size: var(--text-sm);
font-weight: 600;
}
.dump-duplicate-link {
font-size: var(--text-sm);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dump-duplicate-more,
.dump-duplicate-hint {
margin: 0;
font-size: var(--text-xs);
color: var(--color-text-secondary);
}
/* ── Step indicator ── */
.dump-steps {
display: flex;
align-items: center;
gap: 0.5rem;
list-style: none;
margin: 0 0 1.1rem;
padding: 0;
}
.dump-step {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
font-size: var(--text-sm);
color: var(--color-text-muted);
}
/* Connector between steps — drawn on the gap, not on the labels. */
.dump-step + .dump-step::before {
content: "";
width: 1.1rem;
height: 1px;
background: var(--color-border-subtle);
flex-shrink: 0;
}
.dump-step-marker {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.35rem;
height: 1.35rem;
flex-shrink: 0;
border-radius: var(--radius-full);
border: 1.5px solid var(--color-border-subtle);
font-size: var(--text-2xs);
font-weight: 700;
line-height: 1;
}
.dump-step-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dump-step--current {
color: var(--color-text);
font-weight: 600;
}
.dump-step--current .dump-step-marker {
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-on-accent);
}
.dump-step--done {
color: var(--color-text-secondary);
}
.dump-step--done .dump-step-marker {
border-color: var(--color-accent);
color: var(--color-accent);
}
/* Only the current step keeps its label once the modal gets narrow. */
@media (max-width: 30rem) {
.dump-step:not(.dump-step--current) .dump-step-label {
display: none;
}
}
/* ── Panels ── */
.dump-panel {
display: flex;
flex-direction: column;
gap: 1rem; /* matches .form, so panels and plain forms space identically */
animation: dump-panel-in 0.18s ease-out;
}
.dump-panel--back {
animation-name: dump-panel-in-back;
}
@keyframes dump-panel-in {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: none; }
}
@keyframes dump-panel-in-back {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.dump-panel {
animation: none;
}
}
/* One line naming the thing the second panel's choices apply to. */
.dump-panel-recap {
margin: 0;
font-size: var(--text-sm);
color: var(--color-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Mode toggle — segmented control ── */
.visibility-toggle {
display: flex;
background: var(--color-bg);
border: 1.5px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 3px;
gap: 2px;
}
.visibility-toggle button {
flex: 1;
padding: 0.3rem 0.9rem;
border-radius: var(--radius-md);
border: none;
background: transparent;
color: var(--color-text);
cursor: pointer;
font-size: var(--text-sm);
font-weight: 600;
transition: background var(--dur-base), color var(--dur-base);
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 {
border: 2px dashed var(--color-border-subtle);
border-radius: var(--radius-xl);
background: var(--color-surface);
cursor: pointer;
transition: border-color var(--dur-base), background var(--dur-base);
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: var(--text-md);
font-weight: 500;
}
.fdz__browse {
margin: 0;
font-size: var(--text-sm);
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: var(--text-xs);
color: var(--color-text-muted);
}
.fdz__file {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem;
}
.fdz__file-icon {
font-size: var(--text-2xl);
flex-shrink: 0;
}
.fdz__file-meta {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
flex: 1;
}
.fdz__file-name {
font-size: var(--text-md);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fdz__file-size {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.fdz__clear {
flex-shrink: 0;
background: none;
border: none;
cursor: pointer;
color: var(--color-text-muted);
font-size: var(--text-xs);
padding: 0.3rem 0.5rem;
border-radius: var(--radius-sm);
transition: color var(--dur-fast), background var(--dur-fast);
line-height: 1;
}
.fdz__clear:hover {
color: var(--color-text);
background: var(--color-border-subtle);
}
/* ── Upload progress (second panel, while the file is being sent) ── */
.dump-upload-progress {
height: 4px;
border-radius: var(--radius-full);
background: var(--color-border-subtle);
overflow: hidden;
}
.dump-upload-progress-fill {
height: 100%;
width: 0;
border-radius: var(--radius-full);
background: var(--color-accent);
transition: width var(--dur-slow) ease-out;
}
/* Length unknown, or the body is sent and the server is still thinking. */
.dump-upload-progress--indeterminate .dump-upload-progress-fill {
width: 40%;
animation: dump-upload-slide 1.1s ease-in-out infinite;
}
@keyframes dump-upload-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
.dump-upload-progress--indeterminate .dump-upload-progress-fill {
width: 100%;
animation: none;
opacity: 0.5;
}
}
/* ── Local file / URL preview (DumpCreate) ── */
.local-preview-image {
width: 100%;
max-height: 320px;
object-fit: contain;
border-radius: var(--radius-xl);
background: var(--color-bg);
display: block;
}
.preview-loading {
margin: 0;
font-size: var(--text-sm);
opacity: 0.5;
font-style: italic;
}
/* File previews (Dump page) */
.file-preview-image {
max-width: 100%;
border-radius: var(--radius-lg);
display: block;
}
.file-preview-play-btn {
position: relative;
width: 100%;
padding: 0;
border: none;
background: #000;
cursor: pointer;
display: block;
border-radius: var(--radius-lg);
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: var(--text-display);
}
.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: var(--border-width) solid var(--color-border);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
}
/* 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 var(--radius-xl) var(--radius-xl);
overflow: hidden;
border: var(--border-width) 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 var(--radius-xl) var(--radius-xl);
border: var(--border-width) 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 var(--dur-base), transform var(--dur-fast);
}
.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: var(--text-xs);
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: var(--radius-xs);
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: var(--radius-xs);
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 var(--dur-fast) linear;
}
/* Audio whose peaks can't be decoded (cross-origin streams). Boxed to the
waveform's height so the player doesn't resize between item kinds, with the
bar itself centred inside. */
.audio-player-track--stream {
height: 48px;
background: none;
display: flex;
align-items: center;
}
.audio-player-track--stream::before {
content: "";
position: absolute;
inset: 50% 0 auto 0;
transform: translateY(-50%);
height: 6px;
border-radius: var(--radius-xs);
background: color-mix(
in srgb,
var(--color-accent) 12%,
var(--color-border) 88%
);
}
.audio-player-track--stream .audio-player-fill {
top: 50%;
bottom: auto;
transform: translateY(-50%);
height: 6px;
border-radius: var(--radius-xs);
z-index: var(--z-raised);
}
.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: var(--radius-xs);
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 var(--dur-base);
}
.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: var(--radius-lg);
border: var(--border-width) 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: var(--radius-lg);
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
transition: background var(--dur-base), color var(--dur-base);
}
.file-download-link:hover {
background: var(--color-accent);
color: var(--color-on-accent);
}
.dump-file-notice {
margin: 0 0 0.25rem;
font-size: var(--text-md);
opacity: 0.8;
}
.rich-content-card {
display: flex;
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-xl);
overflow: hidden;
transition: border-color var(--dur-slow);
}
.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: var(--z-modal);
background: var(--color-surface);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-3);
animation: player-enter 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both;
transition: opacity var(--dur-slow) ease, box-shadow var(--dur-slow) ease;
}
.global-player--reduced {
opacity: 0.6;
box-shadow: var(--shadow-2);
}
.global-player--reduced:hover {
opacity: 1;
box-shadow: var(--shadow-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: var(--text-md);
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 var(--radius-lg) var(--radius-lg);
}
/* 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;
}
/* ── Global player: queue ── */
/* The title/artist pair takes the space the bare title used to claim. */
.global-player-heading {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.global-player-heading .global-player-title {
flex: none;
}
.global-player-subtitle {
font-size: var(--text-xs);
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.global-player-artwork {
width: 36px;
height: 36px;
flex: none;
object-fit: cover;
border-radius: var(--radius-sm);
}
.global-player-transport {
display: flex;
align-items: center;
gap: 0.25rem;
flex: none;
}
.global-player-position {
font-size: var(--text-xs);
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.global-player-transport .btn--ghost:disabled {
opacity: 0.35;
cursor: default;
}
.global-player-status {
margin: 0;
padding: 0 1rem 0.5rem;
font-size: var(--text-xs);
color: var(--color-text-muted);
}
/* The video already claims most of the viewport, so its list stays shorter. */
.global-player--youtube .global-player-tracks {
max-height: 30vh;
}
.global-player-tracks {
list-style: none;
margin: 0;
padding: 0 0.5rem 0.5rem;
max-height: 40vh;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
}
.global-player-track > button {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.35rem 0.5rem;
background: none;
border: none;
border-left: 3px solid transparent;
color: inherit;
font: inherit;
font-size: var(--text-xs);
text-align: left;
cursor: pointer;
}
.global-player-track > button:hover {
background: var(--color-surface-alt, rgba(127, 127, 127, 0.12));
}
/* A YouTube tracklist is only clickable once its player API answers; until
then the rows are a table of contents. */
.global-player-track > button:disabled {
cursor: default;
}
.global-player-track > button:disabled:hover {
background: none;
}
/* An accent border rather than a filled pill, so no theme has to un-round it. */
.global-player-track.is-current > button {
border-left-color: var(--color-accent);
font-weight: 600;
}
.global-player-track-num {
flex: none;
min-width: 1.5em;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.global-player-track-title {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.global-player-track-dur {
flex: none;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.feed-loading-more {
text-align: center;
padding: 1rem;
color: var(--color-text-muted);
font-size: var(--text-sm);
}
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: var(--z-fab);
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 var(--dur-slow) ease, transform var(--dur-slow) ease, background var(--dur-base) 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: var(--z-fab);
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 var(--dur-slow) ease, transform var(--dur-slow) ease, background var(--dur-base) 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: var(--text-3xl);
transition: background var(--dur-slow) 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: var(--text-2xs);
font-weight: 700;
letter-spacing: 0.04em;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
align-self: flex-start;
}
.rich-content-title {
margin: 0;
font-weight: 600;
font-size: var(--text-base);
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: var(--text-sm);
opacity: 0.75;
-webkit-line-clamp: 2;
line-clamp: 2;
}
.rich-content-url {
font-size: var(--text-xs);
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: var(--radius-sm);
border: 1px solid var(--color-border);
display: block;
}
.rich-content-compact-icon {
font-size: var(--text-base);
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: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text);
font-size: var(--text-sm);
font-weight: 600;
cursor: pointer;
transition: border-color var(--dur-base), background var(--dur-base), color var(--dur-base);
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: var(--text-sm);
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: var(--text-2xl);
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: var(--text-lg);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity var(--dur-base);
}
.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: var(--text-lg);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
cursor: pointer;
transition: opacity var(--dur-base);
}
.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: var(--border-width) 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;
}
}
/* ── Dense dump cards in the two-column profile view ──
The default card is proportioned for the 860px feed. Dropped into a ~400px
column it falls apart: the title wraps four lines deep, the meta row stacks,
every category chip takes a line of its own and the vote button floats in
the middle of the resulting void. Below is the same card re-proportioned for
the column — smaller thumb, title clamped to two lines, no teaser, and meta
plus chips sharing the line under the title. Scoped to the width where the
columns actually split; narrower than that the feed is full-width again and
the regular (then the phone) layout applies. */
@media (min-width: 900px) {
.profile-columns .dump-feed {
padding: 0.75rem 0 0;
gap: 0.5rem;
}
/* preview | body | vote, all top-aligned so the count sits on the title
line instead of drifting to the card's vertical middle. */
.profile-columns .dump-card-inner {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
gap: 0.55rem;
padding: 0.5rem 0.6rem;
}
.profile-columns .dump-card-preview {
grid-area: 1 / 1;
width: 88px;
height: 50px;
}
.profile-columns .dump-card-vote {
grid-area: 1 / 3;
align-self: start;
}
/* Row layout with a full-width title: the title takes a line to itself, then
meta and chips flow onto the next one and only wrap when they must. */
.profile-columns .dump-card-body {
grid-area: 1 / 2;
margin-left: 0;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15rem 0.45rem;
}
.profile-columns .dump-card-title {
flex: 1 0 100%;
font-size: var(--text-md);
line-height: 1.3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
/* The teaser is the first thing to go, the same call the phone layout makes. */
.profile-columns .dump-card-comment {
display: none;
}
.profile-columns .dump-card-meta,
.profile-columns .dump-card-categories {
margin-top: 0;
}
.profile-columns .dump-card-meta {
gap: 0.4rem;
/* Keeps "3 weeks ago" and "1 comment" on one line each. */
white-space: nowrap;
}
.profile-columns .dump-card-date,
.profile-columns .dump-card-comment-count {
font-size: var(--text-2xs);
}
.profile-columns .dump-card-categories {
gap: 0.25rem;
}
.profile-columns .dump-card-category-chip {
padding: 0.05rem 0.4rem;
font-size: var(--text-2xs);
}
.profile-columns .vote-btn {
padding: 0.15rem 0.45rem;
font-size: var(--text-xs);
}
}
.profile-section ul {
list-style: none;
margin: 0;
padding: 0;
}
.empty-state {
margin: 0;
opacity: 0.5;
font-size: var(--text-md);
}
/* ── Profile section header (shared between bare h2 and wrapper div) ── */
.profile-section > h2,
.profile-section-header {
margin-bottom: 0.75rem;
border-bottom: var(--border-width) 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: var(--text-xl);
font-weight: 600;
}
/* Role badge — elevated roles only (see RoleChip) */
.profile-role-chip {
padding: 0.1rem 0.55rem;
border-radius: var(--radius-full);
background: var(--color-accent);
color: var(--color-on-accent);
font-size: var(--text-2xs);
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: var(--radius-md);
font-size: var(--text-md);
}
.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: var(--text-sm);
color: var(--color-muted);
opacity: 0;
transition: opacity var(--dur-fast);
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: var(--text-xs);
color: var(--color-text-muted);
margin: 0.1rem 0 0.4rem;
cursor: pointer;
border-radius: var(--radius-sm);
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: var(--radius-md);
font-size: var(--text-sm);
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: var(--radius-sm);
border: 1.5px solid;
font-size: var(--text-xs);
font-family: inherit;
font-weight: 500;
cursor: pointer;
box-sizing: border-box;
transition: background var(--dur-base), border-color var(--dur-base), color var(--dur-base);
}
.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: var(--text-xs);
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);
}
/* Grid sized by the button row; the invite panel below only fills that
width (width: 0 + min-width: 100% keeps it out of the sizing). */
.profile-own-actions {
display: grid;
width: fit-content;
max-width: 100%;
gap: 0.5rem;
margin-top: 0.5rem;
}
.profile-own-actions-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.profile-own-actions .btn-border {
margin-top: 0;
}
.invite-btn {
padding: 0.3rem 0.9rem;
border: 1.5px solid var(--color-accent);
border-radius: var(--radius-md);
background: transparent;
color: var(--color-accent);
font-size: var(--text-sm);
font-family: inherit;
cursor: pointer;
transition: background var(--dur-base), color var(--dur-base);
}
.invite-btn:hover:not(:disabled),
.invite-btn[aria-expanded="true"] {
background: var(--color-accent);
color: var(--color-on-accent);
}
.invite-btn:disabled {
cursor: progress;
}
/* Raised above the info column's narrow-viewport overflow fade
(.profile-info::after) so the Copy button isn't washed out. */
.invite-panel {
position: relative;
z-index: 1;
width: 0;
min-width: 100%;
box-sizing: border-box;
}
.invite-panel:not(.invite-panel--error) {
display: flex;
flex-direction: column;
padding: 0.5rem;
background: var(--color-surface);
border-radius: var(--radius-md);
}
.invite-panel-hint {
margin-top: 0.4rem;
font-size: var(--text-xs);
color: var(--color-text-muted);
text-align: center;
}
/* Link field and Copy button fused into one control. */
.invite-url {
min-width: 0;
padding: 0.35rem 0.6rem;
border: 1.5px solid var(--color-border);
border-bottom: none;
border-radius: var(--radius-md) var(--radius-md) 0 0;
background: var(--color-surface-sunken);
color: var(--color-text);
font-family: inherit;
font-size: var(--text-sm);
text-overflow: ellipsis;
}
.invite-url:focus {
outline: none;
border-color: var(--color-accent);
}
.invite-copy-btn {
padding: 0.35rem 0.9rem;
border: 1.5px solid var(--color-accent);
border-radius: 0 0 var(--radius-md) var(--radius-md);
background: var(--color-accent);
color: var(--color-on-accent);
font-family: inherit;
font-size: var(--text-sm);
font-weight: 600;
cursor: pointer;
transition: background var(--dur-fast), border-color var(--dur-fast);
}
.invite-copy-btn:hover {
background: var(--color-accent-hover);
border-color: var(--color-accent-hover);
}
.invite-copy-btn--done,
.invite-copy-btn--done:hover {
background: var(--color-success-bg);
border-color: var(--color-success);
color: var(--color-success);
}
/* ── Profile sub-pages (dumps / upvoted / playlists) ── */
.profile-subpage-back {
display: inline-block;
font-size: var(--text-sm);
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: var(--text-2xl);
font-weight: 700;
flex: 1;
}
/* ── Profile "View all" link ── */
.profile-view-all {
display: inline-block;
margin-top: 0.75rem;
font-size: var(--text-sm);
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: var(--radius-md);
background: transparent;
color: var(--color-text-muted);
font-size: var(--text-sm);
font-family: inherit;
cursor: pointer;
transition: border-color var(--dur-base), color var(--dur-base), background var(--dur-base);
}
.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: var(--radius-lg);
font-size: var(--text-md);
}
.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: var(--text-md);
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: var(--radius-xl);
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: var(--text-md);
}
.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(--header-bg);
border-bottom: var(--border-width) 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: var(--text-lg);
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. The reveal wrapper eases
its single grid track between 0fr and 1fr so the row (theme borders and
all) slides open and shut; the clip keeps the collapsed track at zero. */
.header-search-reveal {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows var(--dur-reveal) var(--ease-out),
opacity var(--dur-reveal) var(--ease-out);
}
.header-search-reveal--open {
grid-template-rows: 1fr;
opacity: 1;
}
.header-search-reveal-clip {
min-height: 0;
overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
.header-search-reveal {
transition: none;
}
}
.header-search-below {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
background: var(--header-bg);
border-bottom: var(--border-width) 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-reveal {
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: var(--text-md);
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: var(--text-base);
cursor: pointer;
border-radius: var(--radius-md);
}
.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: var(--text-md);
padding: 0.35rem 0.85rem;
border-radius: var(--radius-lg);
background: var(--color-header-user-bg);
transition: background var(--dur-base);
}
.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: var(--radius-lg);
padding: 0.25rem;
cursor: pointer;
transition: background var(--dur-base);
}
.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: var(--border-width) solid var(--color-border-subtle);
border-radius: var(--radius-xl);
padding: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
z-index: var(--z-sticky);
box-shadow: var(--shadow-2);
}
.user-menu-item {
display: block;
padding: 0.45rem 0.75rem;
border-radius: var(--radius-md);
text-decoration: none;
color: var(--color-text);
font-size: var(--text-md);
font-weight: 600;
transition: background var(--dur-fast);
}
.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;
overscroll-behavior: contain;
scrollbar-width: thin;
background: var(--color-surface);
border: var(--border-width) solid var(--color-border-subtle);
border-radius: var(--radius-xl);
padding: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
z-index: var(--z-sticky);
box-shadow: var(--shadow-2);
}
.user-list-popover-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
border-radius: var(--radius-md);
text-decoration: none;
color: var(--color-text);
font-size: var(--text-sm);
font-weight: 600;
transition: background var(--dur-fast);
}
.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: var(--text-xs);
color: var(--color-text-muted);
}
.user-list-popover-load-more {
background: none;
border: none;
cursor: pointer;
padding: 0.4rem 0.6rem;
font-size: var(--text-xs);
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: var(--border-width) solid var(--color-border);
border-radius: var(--radius-xl);
padding: 2rem;
}
.auth-card-title {
margin: 0;
font-size: var(--text-2xl);
font-weight: 700;
text-align: center;
}
.auth-card-footer {
text-align: center;
font-size: var(--text-md);
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: var(--text-md);
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;
}
/* The edit form spans the page grid and lays out its own panels: the dump
itself (preview, thumbnail, source) beside its presentation (title, why,
categories, visibility), with a full-width action bar underneath so both
columns end on the same line. Only the header and footer are rounded. */
.dump-edit-form {
display: grid;
grid-template-columns: 1fr;
gap: inherit;
}
@media (min-width: 860px) {
.form-page--two-col,
.dump-edit-form {
grid-template-columns: 1fr 1fr;
}
.form-page--two-col .form-page-header,
.dump-edit-form,
.dump-edit-footer {
grid-column: 1 / -1;
}
/* "Why?" soaks up whatever height the source column adds, so a tall
preview doesn't leave a hole under the details. */
.dump-edit-panel--details .form-field:has(.mention-textarea-wrap) {
flex: 1 1 auto;
}
.dump-edit-panel--details .mention-textarea-wrap {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.dump-edit-panel--details .mention-textarea-wrap textarea {
flex: 1 1 auto;
}
}
.form-page-header {
padding: 1.25rem;
background: var(--color-surface);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.form-page-eyebrow {
margin: 0;
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.07em;
opacity: 0.45;
}
.form-page-title {
margin: 0;
font-size: var(--text-2xl);
font-weight: 700;
word-break: break-word;
}
.dump-edit-panel {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0;
padding: 1.25rem;
background: var(--color-surface);
}
.dump-edit-preview {
overflow: hidden;
}
.dump-edit-footer {
padding: 0.875rem 1.25rem;
background: var(--color-surface);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
}
.dump-edit-footer .form-actions {
padding-top: 0;
border-top: 0;
margin-top: 0;
}
.dump-edit-refresh {
margin-top: 0.75rem;
}
.dump-edit-refresh-error {
margin-top: 0.5rem;
font-size: var(--text-sm);
color: var(--color-danger);
}
.dump-edit-thumbnail-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.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: var(--text-md);
color: var(--color-text);
opacity: 0.6;
text-decoration: none;
transition: opacity var(--dur-base);
}
.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: var(--radius-lg);
font-family: inherit;
font-size: var(--text-md);
font-weight: 600;
padding: 0.4rem 1rem;
transition:
background var(--dur-base),
color var(--dur-base),
border-color var(--dur-base),
box-shadow var(--dur-base),
transform var(--dur-fast);
}
.btn-primary {
background: var(--color-accent);
color: var(--color-on-accent);
border: none;
box-shadow: var(--btn-primary-shadow);
}
.btn-primary:hover {
background: var(--color-accent-hover);
box-shadow: var(--btn-primary-shadow-hover);
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);
color: var(--color-on-danger);
}
/* ── 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: var(--radius-md);
color: var(--color-text);
font-family: inherit;
opacity: 0.7;
transition: opacity var(--dur-base), background var(--dur-base);
}
.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: var(--radius-sm);
transition: opacity var(--dur-base), color var(--dur-base);
}
/* ── 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 var(--dur-slow) ease, transform var(--dur-slow) 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;
/* Symmetric, so the tabs stay centred against their row (the header's
brand and buttons), with room on both sides for the focus ring that
overflow-x would otherwise clip. */
padding-block: 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: var(--radius-md);
border: var(--border-width) solid var(--tab-border);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: var(--tab-tracking);
text-transform: var(--tab-text-transform);
transition: border-color var(--dur-base), background var(--dur-base), color var(--dur-base);
white-space: nowrap;
flex-shrink: 0;
}
.feed-sort-btn:hover:not(.active) {
color: var(--color-text);
}
.feed-sort-btn.active {
border-color: var(--tab-active-border, var(--color-accent));
background: var(--tab-active-bg);
color: var(--tab-active-fg);
}
/* ── 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: var(--card-border-width) solid var(--card-border);
border-radius: var(--radius-xl);
background: var(--color-surface);
box-shadow: var(--card-shadow);
min-width: 0;
}
.dump-card:hover,
.playlist-card:hover {
border-color: var(--card-hover-border);
}
/* Card-specific layout */
.dump-card {
display: grid;
grid-template-rows: 1fr;
transition:
border-color var(--dur-base),
grid-template-rows 0.32s ease,
opacity 0.25s ease,
filter 0.3s ease;
}
.playlist-card {
position: relative;
transition: border-color var(--dur-base);
}
/* ── 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;
}
/* The `overflow: hidden` above is there for the dump card's collapse animation
(grid-template-rows: 0fr). A playlist card never collapses, and its height is
set by the 80px square thumb itself — so the clip had nothing to spare on
hover: it shaved the corners off the scaled thumb and sliced its drop shadow
into a hard-edged slab against the card border. Nothing else in the card
needs the clip (the thumb and the description clamp carry their own), so the
lift is simply allowed to render. */
.playlist-card-inner {
overflow: visible;
}
/* 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: var(--radius-md);
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 var(--dur-slow) ease, box-shadow var(--dur-slow) 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: var(--text-3xl);
opacity: 0.7;
line-height: 1;
}
/* ── Generated thumbnail placeholder ──
Stands in for pages that offer no preview image. The site's own color is
mixed *into* the theme surface rather than painted raw, so an arbitrary
third-party accent can never break contrast against --color-text — in any
style, in either color scheme. Themes tune --thumb-tint-strength alone. */
/* Zero-specificity sizing: the placeholder fills its box by default, but any
class it shares with the <img> it replaces (.rich-content-thumbnail's 180px,
.rich-content-compact-thumbnail's 36px) still wins, so it lands exactly where
the image would have. */
:where(.thumb-placeholder) {
width: 100%;
height: 100%;
}
.thumb-placeholder {
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
user-select: none;
color: var(--color-text);
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.12)),
color-mix(
in srgb,
var(--thumb-tint, var(--color-accent)) var(--thumb-tint-strength, 34%),
var(--color-surface)
);
}
.thumb-placeholder-icon {
max-width: 44%;
max-height: 44%;
object-fit: contain;
display: block;
}
.thumb-placeholder-initials {
font-size: var(--text-xl);
font-weight: 800;
line-height: 1;
opacity: 0.75;
letter-spacing: 0.02em;
}
/* The initial has no intrinsic size, so scale it per surface. */
.dump-card-preview .thumb-placeholder-initials,
.playlist-card-preview .thumb-placeholder-initials {
font-size: var(--text-3xl);
}
.journal-card-image .thumb-placeholder-initials {
font-size: var(--text-display);
}
.rich-content-thumbnail.thumb-placeholder .thumb-placeholder-initials {
font-size: var(--text-display);
}
/* `.rich-content-thumbnail` sizes an <img> by width alone and lets the natural
aspect supply the height, which a <div> has none of. Stretching to the card's
height matches the image; the floor keeps it from collapsing when there's
little text, or once the card stacks on narrow screens. */
.rich-content-thumbnail.thumb-placeholder {
height: auto;
align-self: stretch;
min-height: 110px;
}
/* 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 {
position: relative;
width: 100%;
height: 100%;
justify-content: center;
}
/* ── Two targets, one card ──
The card opens the dump page; a playable (or source-linked) thumbnail does
its own thing. Only one of them may look armed at a time: over the thumb
the card's "open" cue (the accent title) stands down and the thumb's own
dim and badge take over. At rest the thumb carries only a small corner
badge, so it reads as "playable" without claiming the whole card. */
.dump-card-inner:has(.dump-card-preview :is(button, a):hover) .dump-card-title {
color: var(--color-text);
}
.dump-card-preview .rich-content-play-overlay,
.rich-content-open-overlay {
position: absolute;
inset: auto auto 5px 5px;
z-index: 1;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: var(--text-xs);
line-height: 1;
pointer-events: none;
transition:
background var(--dur-base) ease,
opacity var(--dur-base) ease,
transform var(--dur-base) ease;
}
/* The badge never moves: card hover grows it a touch; hovering the thumb
itself grows it a touch more while a soft dim fades in behind it. */
.dump-card-inner:hover .dump-card-preview .rich-content-play-overlay {
transform: scale(1.1);
}
.dump-card-preview .rich-content-thumbnail-btn::after,
.dump-card-preview .rich-content-compact::after {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.2);
opacity: 0;
pointer-events: none;
transition: opacity var(--dur-base) ease;
}
/* A link out has nothing to badge at rest — the ↗ only appears on hover. */
.rich-content-open-overlay {
opacity: 0;
}
.dump-card-preview .rich-content-thumbnail-btn:is(:hover, :focus-visible)::after,
.dump-card-preview .rich-content-compact:is(:hover, :focus-visible)::after {
opacity: 1;
}
.dump-card-preview .rich-content-thumbnail-btn:is(:hover, :focus-visible)
.rich-content-play-overlay,
.rich-content-compact:is(:hover, :focus-visible) .rich-content-open-overlay {
transform: scale(1.2);
background: rgba(0, 0, 0, 0.8);
opacity: 1;
}
/* The one playing keeps its badge lit, in the accent, showing ⏸. */
.dump-card-preview .rich-content-thumbnail-btn.is-playing
.rich-content-play-overlay {
background: var(--color-accent);
color: var(--color-on-accent);
}
.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: var(--text-base);
color: var(--color-text);
text-decoration: none;
word-break: break-word;
transition: color var(--dur-base);
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: var(--text-sm);
color: var(--color-text-secondary);
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: var(--text-xs);
color: var(--color-text-muted);
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: var(--text-2xs);
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: var(--text-2xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.45rem;
border-radius: var(--radius-sm);
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: var(--text-xs);
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);
backdrop-filter: var(--overlay-filter);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal);
padding: 1rem;
}
/* Shared modal box surface */
.modal-card,
.confirm-modal {
background: var(--color-surface);
border: var(--border-width) solid var(--modal-border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-3);
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: var(--text-md);
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: var(--text-base);
}
.modal-close-btn {
color: var(--color-text);
font-size: var(--text-base);
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;
overscroll-behavior: contain;
scrollbar-width: thin;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* ── Live chat ── */
/* The chat fills the (now taller) modal so it makes better use of the viewport
on desktop — see the .modal-card:has(.chat) height override below. */
.chat {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
height: 100%;
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. The extra
padding keeps the text clear of the scrollbar rather than right against it. */
scrollbar-gutter: stable;
/* A slim, theme-tinted scrollbar whose thumb stays hidden until the list is
hovered or focused, instead of the heavy native bar. */
scrollbar-width: thin;
scrollbar-color: transparent transparent;
display: flex;
flex-direction: column;
gap: 0.85rem;
padding-right: 0.6rem;
}
.chat-messages:hover,
.chat-messages:focus-within {
scrollbar-color: color-mix(in srgb, var(--color-text-muted) 45%, transparent)
transparent;
}
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 999px;
}
.chat-messages:hover::-webkit-scrollbar-thumb,
.chat-messages:focus-within::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--color-text-muted) 45%, transparent);
}
.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;
}
}
/* Brief highlight when a message is jumped to via a reply reference. */
.chat-message {
border-radius: var(--radius-lg);
transition: background-color 0.4s ease;
}
.chat-message--flash {
background-color: color-mix(in srgb, var(--color-accent) 18%, transparent);
}
/* Subtle day separator: a hairline rule with a centered, muted label. */
.chat-day-sep {
display: flex;
align-items: center;
gap: 0.6rem;
margin: 0.15rem 0;
color: var(--color-text-muted);
}
.chat-day-sep::before,
.chat-day-sep::after {
content: "";
flex: 1;
height: 1px;
background: var(--color-border);
}
.chat-day-sep-label {
font-size: var(--text-2xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.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: var(--text-sm);
color: inherit;
text-decoration: none;
}
.chat-message-author:hover {
color: var(--color-accent);
}
.chat-message-time {
font-size: var(--text-2xs);
color: var(--color-text-muted);
}
.chat-message-text {
font-size: var(--text-md);
}
.chat-message-text.md {
line-height: 1.45;
}
.chat-message-edited {
font-size: var(--text-2xs);
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 var(--dur-base);
}
.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: var(--text-xs);
line-height: 1;
color: var(--color-text-muted);
cursor: pointer;
transition: color var(--dur-base);
}
.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: var(--text-xs);
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;
}
/* ── Reply reference (subtle, above a message's body) ── */
.chat-reply-ref {
display: flex;
align-items: baseline;
gap: 0.3rem;
max-width: 100%;
margin-bottom: 0.15rem;
padding: 0;
background: none;
border: none;
font: inherit;
font-size: var(--text-xs);
color: var(--color-text-muted);
cursor: pointer;
text-align: left;
}
.chat-reply-ref:disabled {
cursor: default;
font-style: italic;
}
.chat-reply-ref:not(:disabled):hover .chat-reply-ref-author,
.chat-reply-ref:not(:disabled):hover .chat-reply-ref-text {
color: var(--color-text);
}
.chat-reply-ref-mark {
opacity: 0.7;
flex-shrink: 0;
}
.chat-reply-ref-author {
font-weight: 700;
flex-shrink: 0;
transition: color var(--dur-base);
}
.chat-reply-ref-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.85;
transition: color var(--dur-base);
}
/* ── Reply composer bar (subtle, above the input) ── */
.chat-reply-bar {
display: flex;
align-items: center;
gap: 0.4rem;
flex-shrink: 0;
padding: 0.35rem 0.6rem;
border-left: 2px solid var(--color-accent);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.chat-reply-bar-mark {
opacity: 0.7;
flex-shrink: 0;
}
.chat-reply-bar-text {
display: flex;
align-items: baseline;
gap: 0.3rem;
min-width: 0;
flex: 1;
}
.chat-reply-bar-author {
font-weight: 700;
color: var(--color-text);
flex-shrink: 0;
}
.chat-reply-bar-snippet {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.8;
}
.chat-reply-bar-cancel {
margin-left: auto;
flex-shrink: 0;
font-size: var(--text-sm);
}
/* ── "Is typing" indicator (reuses presence avatars) ── */
.chat-typing {
display: flex;
align-items: center;
gap: 0.45rem;
flex-shrink: 0;
min-height: 1.25rem;
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.chat-typing-avatars {
display: inline-flex;
}
/* Overlap multiple typers' avatars slightly, like a stacked presence cluster. */
.chat-typing-avatar:not(:first-child) {
margin-left: -0.4rem;
}
.chat-typing-avatar {
border-radius: 50%;
box-shadow: 0 0 0 2px var(--color-surface);
}
.chat-typing-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-typing-dots {
display: inline-flex;
gap: 2px;
flex-shrink: 0;
}
.chat-typing-dots span {
width: 4px;
height: 4px;
border-radius: 50%;
background: currentColor;
opacity: 0.4;
animation: chat-typing-bounce 1.2s infinite ease-in-out;
}
.chat-typing-dots span:nth-child(2) {
animation-delay: 0.15s;
}
.chat-typing-dots span:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes chat-typing-bounce {
0%,
60%,
100% {
opacity: 0.3;
transform: translateY(0);
}
30% {
opacity: 0.9;
transform: translateY(-2px);
}
}
@media (prefers-reduced-motion: reduce) {
.chat-typing-dots span {
animation: none;
}
}
/* Give the chat a tall, stable panel so it uses the viewport well on desktop
instead of floating as a small box. Bounded by max-height so it never spills
off short screens; the inner message list scrolls within it. */
.modal-card:has(.chat) {
height: min(86vh, 820px);
max-height: 92vh;
}
/* The chat sizes itself to the modal and the message list carries its own
scroll, so the body never has anything to scroll — dropping its clip keeps a
rounding overflow from raising a second, empty scrollbar beside the messages.
(The composer's popovers no longer need this: they are portaled to <body>.) */
.modal-card:has(.chat) .modal-body {
overflow: visible;
}
/* 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: var(--text-md);
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: var(--radius-lg);
cursor: pointer;
transition: background var(--dur-base);
}
.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: var(--text-base);
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: var(--radius-lg);
color: var(--color-accent);
cursor: pointer;
padding: 0.6rem 1rem;
font-size: var(--text-md);
text-align: left;
width: 100%;
transition: border-color var(--dur-base), background var(--dur-base);
}
.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: var(--radius-xl);
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: var(--radius-lg);
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: var(--text-2xl);
font-weight: 700;
word-break: break-word;
}
.playlist-detail-description {
font-size: var(--text-base);
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: var(--text-sm);
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: var(--radius-md);
cursor: pointer;
color: var(--color-text);
font-size: var(--text-sm);
padding: 0.25rem 0.7rem;
opacity: 0.7;
transition: opacity var(--dur-base), border-color var(--dur-base), color var(--dur-base);
}
.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: var(--text-md);
padding: 0.4rem 0.65rem;
}
.playlist-edit-textarea {
resize: none;
overflow: hidden;
line-height: 1.5;
}
.playlist-edit-input {
font-size: var(--text-lg);
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 var(--dur-base);
border-radius: var(--radius-xl);
}
.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: var(--text-sm);
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: var(--text-xs);
padding: 0.2rem 0.5rem;
border-radius: var(--radius-sm);
transition: opacity var(--dur-base), border-color var(--dur-base), color var(--dur-base);
white-space: nowrap;
}
.drag-handle {
cursor: grab;
opacity: 0.35;
font-size: var(--text-lg);
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: var(--border-width) solid var(--color-border);
border-radius: var(--radius-lg);
background: none;
cursor: pointer;
color: var(--color-text);
font-size: var(--text-xs);
font-weight: 600;
white-space: nowrap;
transition: border-color var(--dur-base), color var(--dur-base);
}
.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: var(--text-sm);
padding: 0;
transition: opacity var(--dur-base);
white-space: nowrap;
}
.new-item-toggle:hover {
opacity: 0.75;
}
/* ── Dump card comment count ── */
.dump-card-comment-count {
font-size: var(--text-2xs);
color: var(--color-text-muted);
}
/* ── Dump card private badge ── */
.dump-card-private-badge {
font-size: var(--text-2xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.1em 0.45em;
border-radius: var(--radius-sm);
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: var(--border-width) solid var(--color-border);
}
.comment-section-title,
.related-section-title {
font-size: var(--text-xs);
font-weight: 700;
color: var(--color-text-muted);
margin: 0 0 1.25rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* Backlinks are supporting context, not the main event — render them as a
tighter, lighter list so they stay scannable but clearly subordinate to
the dump's own content. */
.related-section .related-section-title {
margin-bottom: 0.85rem;
}
.related-section .dump-feed {
padding: 0;
gap: 0.4rem;
max-width: none;
}
.related-section .dump-card {
border-width: 1px;
border-radius: var(--radius-lg);
background: transparent;
opacity: 0.78;
transition: border-color var(--dur-base), opacity var(--dur-base);
}
.related-section .dump-card:hover {
opacity: 1;
}
.related-section .dump-card-inner {
padding: 0.45rem 0.65rem;
gap: 0.6rem;
}
.related-section .dump-card-preview {
width: 76px;
height: 43px;
align-self: center;
}
.related-section .dump-card-title {
font-size: var(--text-sm);
}
/* Keep just enough description to orient, but only a single muted line. */
.related-section .dump-card-comment {
font-size: var(--text-xs);
-webkit-line-clamp: 1;
line-clamp: 1;
}
@media (max-width: 512px) {
.related-section .dump-card-preview {
width: 64px;
height: 36px;
}
}
/* "In collections" reuses the related-section chrome (and its per-theme
overrides), so the playlist cards inside it get the same subordinate,
tightened treatment the backlink dump cards do. */
.collections-section .playlist-card {
border-width: 1px;
border-radius: var(--radius-lg);
background: transparent;
opacity: 0.78;
transition: border-color var(--dur-base), opacity var(--dur-base);
}
.collections-section .playlist-card:hover {
opacity: 1;
}
.collections-section .playlist-card-inner {
padding: 0.45rem 0.65rem;
gap: 0.6rem;
}
.collections-section .playlist-card-preview {
width: 48px;
height: 48px;
align-self: center;
}
.collections-section .playlist-card-icon {
font-size: var(--text-lg);
}
.collections-section .playlist-card-title {
font-size: var(--text-sm);
}
.collections-section .playlist-card-description {
font-size: var(--text-xs);
-webkit-line-clamp: 1;
line-clamp: 1;
}
@media (max-width: 512px) {
.collections-section .playlist-card-preview {
width: 40px;
height: 40px;
}
}
.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: var(--radius-lg);
background: var(--color-surface);
transition: background var(--dur-fast);
}
.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: var(--text-sm);
color: var(--color-accent);
text-decoration: none;
}
.comment-author:hover {
text-decoration: underline;
}
.comment-time {
font-size: var(--text-xs);
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: var(--radius-md);
animation: comment-highlight 2s ease-out forwards;
}
.comment-edited {
font-size: var(--text-2xs);
color: var(--color-text-muted);
opacity: 0.7;
font-style: italic;
}
.comment-body {
font-size: var(--text-md);
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: var(--radius-sm);
padding: 0.15rem 0.45rem;
font-size: var(--text-xs);
color: var(--color-text-muted);
cursor: pointer;
font-family: inherit;
transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.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: var(--radius-lg);
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: var(--radius-md);
padding: 0.55rem 0.75rem;
font-family: inherit;
font-size: var(--text-sm);
color: var(--color-text);
resize: none;
overflow: hidden;
min-height: 2.4rem;
transition: border-color var(--dur-base), box-shadow var(--dur-base);
}
.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: var(--radius-md);
padding: 0.4rem 1rem;
font-family: inherit;
font-size: var(--text-sm);
font-weight: 700;
cursor: pointer;
transition: background var(--dur-base), opacity var(--dur-base);
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: var(--radius-md);
padding: 0.4rem 1rem;
font-family: inherit;
font-size: var(--text-sm);
color: var(--color-text);
opacity: 0.6;
cursor: pointer;
transition: opacity var(--dur-base);
}
.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: var(--text-sm);
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: var(--radius-md);
}
.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: var(--text-md);
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: var(--text-md);
font-weight: 600;
}
.invite-tree-user:hover {
color: var(--color-accent);
}
.invite-tree-empty {
color: var(--color-text-muted);
font-size: var(--text-md);
}
/* ── Follow button ── */
.follow-btn {
padding: 0.25rem 0.9rem;
border-radius: var(--radius-md);
border: 2px solid var(--color-accent);
background: transparent;
color: var(--color-accent);
cursor: pointer;
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
font-family: inherit;
transition: background var(--dur-base), color var(--dur-base);
}
.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. The composer clamps every footprint to the cells left in its row,
so the four columns close on every row with no gap to backfill — and
without `dense` moving a later card up into one, the order cards are read
in stays the order they were ranked in. */
.journal-grid {
list-style: none;
margin: 0 auto;
padding: 1.25rem;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 9rem;
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: var(--card-border-width) solid var(--card-border);
border-radius: var(--radius-xl);
background: var(--color-surface);
overflow: hidden;
cursor: pointer;
min-width: 0;
transition: border-color var(--dur-base), transform var(--dur-base), box-shadow var(--dur-base);
}
.journal-card:hover {
border-color: var(--card-hover-border);
transform: translateY(-2px);
box-shadow: var(--shadow-2);
}
.journal-card-title {
font-weight: 600;
font-size: var(--text-md);
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: var(--text-xs);
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.
Here the card *is* the play button and only the title opens the dump. The
badge sits in the top corner, clear of the text on every card size, and
speaks the list view's language: card hover wakes it a touch, and it
settles back while the pointer is on the title or the vote. */
.journal-card--image .rich-content-play-overlay {
pointer-events: none;
inset: 0.6rem 0.6rem auto auto;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
/* Keeps it findable on black letterbox bars. */
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
font-size: var(--text-xs);
line-height: 1;
transition:
background var(--dur-base) ease,
transform var(--dur-base) ease;
}
.journal-card--image:hover .rich-content-play-overlay {
transform: scale(1.15);
background: rgba(0, 0, 0, 0.75);
}
.journal-card--image:has(.journal-card-title:hover, .journal-card-vote:hover)
.rich-content-play-overlay {
transform: none;
background: rgba(0, 0, 0, 0.6);
}
.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);
}
/* The resting vote button is transparent and inks itself in --color-text,
which in a light palette (or a muted dark one) sinks into the black scrim.
Over an image it takes the overlay's own white-on-glass treatment; the
voted state keeps its solid accent fill, which already reads. */
.journal-card--image .journal-card-overlay .vote-btn:not(.vote-btn--active) {
color: #fff;
border-color: rgba(255, 255, 255, 0.4);
background: rgba(0, 0, 0, 0.35);
}
.journal-card--image .journal-card-overlay .vote-btn:not(.vote-btn--active):hover:not(:disabled) {
color: var(--color-accent);
border-color: var(--color-accent);
}
.journal-card--image .journal-card-comment {
font-size: var(--text-sm);
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: var(--text-xl);
-webkit-line-clamp: 3;
line-clamp: 3;
}
.journal-card--square.journal-card--image .journal-card-title {
font-size: var(--text-sm);
}
/* ── Quote cards — the note is the subject, on an accent wash ── */
.journal-card--quote {
--journal-quote-pad: 0.9rem;
padding: var(--journal-quote-pad);
gap: 0.4rem;
background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
}
/* Two things had this mark reading as a smudge in the corner rather than as
the note's own opening quote. The card clips its overflow, so hanging the
glyph off the top edge didn't bleed it past the corner — it cut away the
half that carries the ink. And a fixed offset put what was left outside the
text column, floating against the card's edge instead of lining up with the
words it belongs to. So: keep the whole glyph inside the card, and set it
against the same padding the note is set against. The leading above the ink
is what the top offset takes back, so the mark starts on the note's first
line rather than below it. */
.journal-card-quotemark {
position: absolute;
top: calc(var(--journal-quote-pad) - 0.75rem);
right: var(--journal-quote-pad);
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: var(--z-raised);
margin: 0;
font-size: var(--text-md);
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: var(--text-base);
-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: var(--text-xs);
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: var(--text-2xl);
line-height: 1;
}
.journal-card--text .journal-card-title {
font-size: var(--text-md);
-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: var(--text-sm);
color: var(--color-text-secondary);
opacity: 0.55;
}
/* Below the full width the composer's four-column tiling no longer describes
the grid, so a banner can meet a single free cell and wrap. Dense auto-flow
comes back to backfill what that leaves — worth the reordering here, where
the alternative is a hole. */
@media (max-width: 980px) {
.journal-grid {
grid-template-columns: repeat(3, 1fr);
grid-auto-flow: dense;
}
}
@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: var(--text-md);
font-weight: 600;
padding: 0.35rem 0.85rem;
border-radius: var(--radius-lg);
transition: background var(--dur-base);
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: var(--text-2xs);
font-weight: 700;
min-width: 16px;
height: 16px;
border-radius: var(--radius-lg);
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: var(--text-md);
font-weight: 600;
padding: 0.35rem 0.85rem;
border-radius: var(--radius-lg);
transition: background var(--dur-base);
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: var(--text-2xl);
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.notifications-title-bell {
font-size: var(--text-lg);
}
.notifications-unread-pill {
font-size: var(--text-xs);
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: var(--radius-xl);
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: var(--text-display);
display: block;
margin-bottom: 0.75rem;
opacity: 0.5;
}
.notifications-empty p {
margin: 0.25rem 0;
}
.notifications-empty-hint {
font-size: var(--text-sm);
max-width: 340px;
margin: 0.5rem auto 0 !important;
line-height: 1.5;
}
.notif-group {
margin-bottom: 2rem;
}
.notif-group-label {
font-size: var(--text-2xs);
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: var(--radius-xl);
border: 1px solid var(--color-border-subtle);
border-left: 3px solid transparent;
transition: background var(--dur-fast), border-color var(--dur-fast);
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: var(--text-md);
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: var(--text-sm);
line-height: 1.5;
min-width: 0;
}
.notification-time {
font-size: var(--text-2xs);
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: var(--radius-md);
cursor: pointer;
font-size: var(--text-sm);
color: var(--color-text-muted);
transition: border-color var(--dur-base), color var(--dur-base);
}
.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: var(--text-2xs);
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;
}
/* Portaled to <body> and positioned from JS (see useAnchoredFloat), for the
same reason as the emoji picker below: the editor sits inside cards, modal
bodies and scrollers whose overflow would clip an absolutely positioned
popup. Above the modal backdrop (1000), below tooltips (9999). */
.mention-dropdown {
position: fixed;
z-index: var(--z-popover);
max-width: calc(100vw - 16px);
list-style: none;
margin: 0;
padding: 4px 0;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-2);
max-height: var(--float-max-h, 220px);
overflow-y: auto;
}
.mention-dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
cursor: pointer;
transition: background var(--dur-fast);
}
.mention-dropdown-item:hover,
.mention-dropdown-item--selected {
background: var(--color-bg);
}
.mention-dropdown-username {
font-size: var(--text-md);
color: var(--color-text);
}
/* ── Emoji picker (frimousse) ── */
/* Portaled to <body> and positioned from JS (see useAnchoredFloat): anchored
with `position: fixed` so cards, modal bodies and scrollers can't clip it.
Above the modal backdrop (1000), below tooltips (9999). */
.emoji-picker-float {
position: fixed;
z-index: var(--z-popover);
width: 320px;
max-width: calc(100vw - 16px);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-2);
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: var(--text-md);
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: var(--text-xs);
line-height: 1;
}
.emoji-picker-close-btn:hover {
opacity: 1;
}
/* frimousse uses bare attributes (no data- prefix) */
.emoji-picker-float [frimousse-viewport] {
max-height: var(--float-max-h, 220px);
/* frimousse already sets overflow-y: auto inline */
}
.emoji-picker-float [frimousse-category-header] {
padding: 4px 8px 2px;
font-size: var(--text-xs);
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: var(--radius-sm);
transition: background var(--dur-fast);
}
.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: var(--text-sm);
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: var(--radius-sm);
font-size: var(--text-xs);
font-style: normal;
white-space: nowrap;
pointer-events: none;
z-index: var(--z-toast);
box-shadow: var(--shadow-2);
/* The inline `transform` places it above the anchor, so the entrance rides
on the independent `translate`/`scale` properties, which compose with it.
A short delay keeps it from flashing while the pointer sweeps across. */
transform-origin: 50% 100%;
animation: tooltip-in 160ms cubic-bezier(0.2, 0.7, 0.3, 1) 120ms both;
}
@keyframes tooltip-in {
from { opacity: 0; translate: 0 4px; scale: 0.96; }
to { opacity: 1; translate: 0 0; scale: 1; }
}
@media (prefers-reduced-motion: reduce) {
.tooltip {
animation-name: tooltip-fade;
}
@keyframes tooltip-fade {
from { opacity: 0; }
}
}
/* ── 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: var(--radius-lg);
border: var(--border-width) solid var(--color-border-subtle);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-sm);
font-family: inherit;
outline: none;
transition:
max-width 0.25s ease,
opacity var(--dur-slow) ease,
padding 0.25s ease,
border-width 0.25s ease,
border-color var(--dur-base);
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: var(--border-width) solid var(--color-border-subtle);
border-radius: var(--radius-lg);
cursor: pointer;
font-size: var(--text-sm);
flex-shrink: 0;
transition: border-color var(--dur-base);
}
.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: var(--text-md);
}
/* ── 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: var(--radius-xl);
border: var(--border-width) solid var(--color-border-subtle);
text-decoration: none;
color: var(--color-text);
transition: border-color var(--dur-base);
}
.user-result-item:hover {
border-color: var(--color-accent);
}
.user-result-name {
font-weight: 600;
font-size: var(--text-md);
}
.user-result-description {
font-size: var(--text-sm);
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: var(--text-sm);
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 var(--dur-base) 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: var(--radius-xs);
flex-shrink: 0;
}
/* Themed popover (fixed-positioned so the row's overflow can't clip it). */
.category-switcher-menu {
position: fixed;
z-index: var(--z-dropdown);
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: var(--border-width) solid var(--color-border-subtle);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-2);
}
.category-switcher-option {
display: block;
width: 100%;
text-align: left;
padding: 0.45rem 0.75rem;
border: none;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text);
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
transition: background var(--dur-fast);
}
.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: var(--radius-full);
background: var(--color-bg);
color: var(--color-text-secondary);
font-size: var(--text-sm);
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: var(--radius-full);
background: var(--color-header-user-bg);
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-xs);
}
.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: var(--radius-full);
background: var(--color-bg);
color: var(--color-text);
font-size: var(--text-md);
font-family: inherit;
line-height: 1.4;
cursor: pointer;
transition: border-color var(--dur-base), background var(--dur-base), color var(--dur-base), transform var(--dur-fast);
}
.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: var(--radius-full);
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: var(--text-md);
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: var(--radius-full);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--text-xs);
line-height: 1;
cursor: pointer;
transition: background var(--dur-base), color var(--dur-base), filter var(--dur-base);
}
.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);
}