/* Styles specific to the public pages (/browse, /tag/NAME, /bookmark/:id).
   Everything else — .toolbar, .count, .list, .row, .row-title-line,
   .row-url-line, .row-favicon, .row-sitename, .row-description, .row-thumb,
   .empty, .btn, .tag-pill, .tag-banner — comes from styles.css, which the
   dashboard already established these read-only rows (and its own tag
   filter banner) are visually a variant of. */

.row-tags-line {
  margin-top: 6px;
}

/* ---- ad slots (public/ads.js) ---- */

/* Shared by every page with an ad slot — originally just #adSlot on
   /view/:id's video-embed mode (frame.html/frame.js), moved here now that
   /browse (and /tag/NAME, /:username), /tags, and the bookmark permalink
   page each got one of their own (lib/adSlots.js's browse_top/tags_top/
   bookmark_top) — all of them already load this file, so one shared rule
   covers every placement instead of repeating it per page. No border/
   background/min-height of its own on purpose: whatever the ad network's
   embed code renders supplies its own look, and an empty, unconfigured slot
   (the common case until an admin sets one up — see /admin) should take up
   no visible space at all rather than showing as a blank box. */
.ad-slot {
  /* Explicit width, not just max-width — on frame.html specifically, body
     is `display: flex; flex-direction: column` (see frame.css), and a flex
     item with an auto cross-axis margin (the "margin: ... auto" centering
     trick below) doesn't stretch to fill the container the way a normal
     block box would; it shrink-wraps to its own content instead, which
     silently broke every section using this exact pattern in video-embed
     mode (this ad slot, .bm-card/#videoCard, .comments-section, and
     frame.css's .video-stats-bar/.related-grid) — a comment box rendering
     at ~200px instead of the intended 640px column was the visible
     symptom that led here. width: 100% (capped by max-width, then
     centered by the auto margins) is a no-op everywhere else that loads
     this file, since normal block layout already computes width:auto the
     same way — it only matters inside this flex context. */
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  /* margin:auto above only ever centers the ad-slot BOX itself — the ad
     network's own markup rendered inside it (an <ins>, an <iframe>, a
     <div> the network's script builds) doesn't necessarily fill that box's
     full width, and needs its own centering. `text-align: center` was the
     first thing tried here, but it only affects INLINE content — most real
     ad markup (an <ins> with a script-assigned fixed pixel width, or any
     <div>/<iframe> a network's own JS ends up rendering, both block-level
     by default) sat flush left regardless, confirmed by actually testing a
     saved embed rather than assuming. Flex + align-items:center centers a
     child regardless of whether that child turns out to be inline or
     block, which is what actually fixes it for arbitrary third-party
     markup instead of only the inline case. */
  display: flex;
  flex-direction: column;
  align-items: center;
}

.ad-slot:not(:empty) {
  margin-bottom: 24px;
}

/* Per-slot opt-in (lib/adSlots.js's full_width column, toggled from /admin's
   "Full width" checkbox) — applied by public/ads.js's initAdSlot based on
   the fetched slot's own config, not something set in any page's static
   markup. Just removes the 640px cap above; .ad-slot's other rules (the
   flex centering, the :not(:empty) margins, the iframe/img/ins max-width
   fix below) all still apply unchanged, so a full-width slot still shrinks
   gracefully on a narrow screen and still takes no space when empty. How
   wide "full width" actually ends up being depends on this element's own
   parent in each page's markup (e.g. .container's 860px on a plain content
   page) — this doesn't reach past that, it just stops capping below it. */
.ad-slot.ad-slot-full {
  max-width: none;
}

/* An ad slot that sits flush against something directly above it with no
   heading/toolbar row of its own to create natural breathing room —
   currently the bookmark permalink page's #adSlotTop (lib/publicPages.js,
   flush against the site header) and /view/:id's #adSlotToolbar
   (frame.html, flush against the sticky frame toolbar). /browse's,
   /tags's, and /:username's own top slots don't need this — each already
   has a search/sort toolbar or heading row above it providing that gap.
   Also :not(:empty)-gated, same reasoning as the bottom margin above: an
   empty, unconfigured slot should take up no visible space at all, margin
   included, not leave a gap where an ad would go. */
.ad-slot-page-top:not(:empty) {
  margin-top: 20px;
}

/* Most ad networks hand back a fixed-pixel-width iframe/ins/img in their
   embed code (a 300x250 box, a 728x90 leaderboard, …) — fine on desktop,
   where .ad-slot's own 640px column is comfortably wider than any standard
   ad unit, but a leaderboard-width creative is wider than a phone's own
   viewport, and without this it forces the WHOLE PAGE to scroll
   horizontally to show it, not just the ad. This doesn't make the ad's
   actual creative reflow (most ad networks don't support that, and
   stretching a fixed-size creative's height to match a scaled-down width
   would just distort it) — it caps whatever the embed renders at the
   slot's own width and lets the browser scale it down, which is enough to
   stop it from breaking the rest of the page's layout on a small screen. */
.ad-slot iframe,
.ad-slot img,
.ad-slot ins {
  max-width: 100%;
  height: auto;
}

/* ---- tag pill expand/collapse (public/browse.js's buildTagsHtml) ---- */

/* .tags-extra is a plain inline wrapper around the overflow .tag-pill
   links — no layout rules of its own needed, its children already flow
   exactly like the always-visible pills before it; [hidden] (the native
   HTML attribute, toggled by initTagToggles) is what actually shows/hides
   it. */
.tag-toggle {
  display: inline-flex;
  align-items: center;
  background: none;
  border: 1px solid var(--border);
  color: var(--text-muted);
  border-radius: 999px;
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  margin: 2px 4px 2px 0;
  vertical-align: middle;
}

.tag-toggle:hover {
  background: var(--accent-soft);
  color: var(--text);
  border-color: var(--accent-soft);
}

.row-meta {
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-muted);
}

/* ---- thumbnail placeholder ---- */

/* Shown in a bookmark's .row-thumb slot instead of a real <img> whenever it
   has no captured image_url, or a captured one fails to load in the
   browser — public/browse.js's thumbPlaceholderHtml() and
   lib/publicPages.js's own copy for the permalink page. Same .row-thumb
   class as a real image, so it automatically picks up identical
   sizing/positioning in both list view (72px square, styles.css) and grid
   view (full-width bled card header, this file's .list.grid-view
   .row-thumb) — this rule only adds the placeholder's own look on top. */
.row-thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--text-muted);
}

/* This element's own `display: flex` above is an author style, which wins
   the cascade over the browser's default `[hidden]{display:none}` (a
   user-agent style) at equal specificity — so without this rule, setting
   the `hidden` attribute (as the broken-image fallback in
   public/browse.js's renderList does) would do nothing and the
   placeholder would stay visibly flexed even while hidden. */
.row-thumb-placeholder[hidden] {
  display: none;
}

.row-thumb-placeholder svg {
  width: 40%;
  height: 40%;
  max-width: 32px;
  max-height: 32px;
}

/* ---- profile follow bar (/:username only) ---- */

.profile-meta {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 14px;
}

/* Wraps memberSince + profileCounts so they stack as their own two-line
   column (X.com-style: join date above, follower/following counts below)
   instead of competing with the Follow button for the same row. */
.profile-meta-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.profile-counts {
  font-size: 13px;
  color: var(--text-muted);
}

.profile-joined {
  font-size: 13px;
  color: var(--text-muted);
}

.profile-joined:empty {
  display: none;
}

/* Unfollowed state reuses .btn-primary (solid accent) so it reads as the
   inviting default action; the "following" modifier below flips that to an
   outline once clicked, the common social-app convention for "you're
   already doing this, click to undo" rather than looking like a second
   equally-weighted call to action. */
#followBtn.following {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
}

/* ---- sort toggle (Newest / Top / Following) ---- */

.sort-toggle {
  display: flex;
  gap: 4px;
  flex: 0 0 auto;
}

.sort-btn {
  padding: 9px 14px;
  font-size: 13px;
}

.sort-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

/* ---- List/Grid layout toggle ---- */

/* Same active-state look as .sort-btn above, just narrower — these two
   buttons are icons, not words, so they don't need .sort-btn's wider
   horizontal padding. */
.view-toggle {
  display: flex;
  gap: 4px;
  flex: 0 0 auto;
}

.view-toggle-btn {
  padding: 9px 12px;
  font-size: 14px;
  line-height: 1;
}

.view-toggle-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

/* Grid view reuses the exact same .row markup list view renders
   (browse.js's renderList never branches on view mode) — everything below
   is pure reflow. flex `order` moves the thumbnail from its list-view spot
   on the right to the top of the card, and drops the vote widget out of
   the left margin into its own row underneath the text. A card with no
   thumbnail (item.image_url unset) just renders shorter, the same as a
   thumbnail-less row already does in list view. */
.list.grid-view {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}

.list.grid-view .row {
  flex-direction: column;
  align-items: stretch;
}

/* Bleeds the thumbnail to the card's edges using the row's own padding
   (12px 14px, from styles.css's .row) as the bleed amount, so it reads as
   a real image card header rather than an inset picture. */
.list.grid-view .row-thumb {
  order: -1;
  width: calc(100% + 28px);
  height: 180px;
  margin: -12px -14px 10px;
  border-radius: 10px 10px 0 0;
  border: none;
}

.list.grid-view .row-main {
  order: 1;
}

.list.grid-view .vote-widget {
  order: 2;
  flex-direction: row;
  justify-content: flex-start;
  gap: 8px;
  margin-top: 10px;
}

/* .row-tags only ever exists on the Dashboard's own rows (public/
   dashboard.js) — a <textarea> now, not an <input> (see that file's own
   comment on the swap), so it already wraps a long tag list onto multiple
   lines instead of clipping/scrolling horizontally the way the old
   single-line input did. This just gives it more room to actually show
   those wrapped lines by default in the narrower grid card column, rather
   than starting at the same one-line height list view keeps (rows="1" in
   the markup) and relying on someone noticing the resize handle
   (.row-tags/.row-notes's shared resize: vertical, styles.css). List view
   is untouched — its rows are already wide enough that most tag lists fit
   on one line anyway. */
.list.grid-view .row-tags {
  min-height: 54px;
}

/* .row-delete only ever exists on the Dashboard's own rows (styles.css) —
   public/browse.js's rows have no delete action, so this never matches
   there. Without an explicit order, it would land between the thumbnail
   (order -1) and .row-main (order 1) in the column flow above — a full-
   width "✕" sliver wedged between the image and the text, which reads as
   broken rather than as a delete control. Overlaying it on the thumbnail's
   own top-right corner instead (the common "remove this card" convention)
   keeps it out of the card's text flow entirely. A thumbnail-less row still
   has something to overlay onto — thumbPlaceholderHtml's placeholder span
   carries the same .row-thumb class as a real image (dashboard.js), so it
   gets the exact same 180px card-header treatment above. */
.list.grid-view .row {
  position: relative;
}

.list.grid-view .row-delete {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  z-index: 1;
}

.list.grid-view .row-delete:hover {
  background: #c62828;
}

/* ---- up/down vote widget ---- */

/* Shown wherever a public bookmark is listed — /browse, /tag/:name,
   /:username row lists (public/browse.js) and the /bookmark/:id permalink
   page (lib/publicPages.js's renderVoteWidget, with the .horizontal
   modifier below). Not shown on the dashboard — voting is a public/social
   action on someone ELSE's content; you can't vote on your own bookmark
   anyway (routes/votes.js rejects it, and app.js hides the widget for it).
   Colors are fixed hex, not var(--accent) — --accent flips to a pink/red in
   dark mode (see styles.css's :root[data-theme="dark"]), which would read
   oddly next to a red downvote, the same reason .row-delete's hover
   (styles.css) uses a fixed red rather than the theme accent. */
.vote-widget {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
}

.vote-widget.horizontal {
  flex-direction: row;
  gap: 8px;
  margin-bottom: 14px;
}

.vote-btn {
  background: none;
  border: none;
  padding: 2px 4px;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1;
  border-radius: 4px;
  font-family: inherit;
}

.vote-btn:hover:not(:disabled) {
  background: var(--accent-soft);
}

.vote-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.vote-up.vote-active {
  color: #2e7d32;
}

.vote-down.vote-active {
  color: #c62828;
}

.vote-score {
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
  min-width: 1.4em;
  text-align: center;
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin: 24px 0 8px;
}

.pagination button {
  cursor: pointer;
}

.pagination button:disabled {
  opacity: 0.4;
  cursor: default;
}

#pageInfo {
  font-size: 13px;
  color: var(--text-muted);
}

/* ---- tag directory (/tags) ---- */

/* CSS multi-column rather than a grid/flex-wrap — the tag list is a single
   alphabetical sequence (not a set of independent cards), and columns keep
   it reading top-to-bottom-then-across, like a phone book or an index,
   which is the order someone scanning for a specific letter expects.
   column-width (not column-count) lets the browser pick however many
   columns actually fit at the current window size — 2 on a narrow phone,
   4+ on a wide desktop — rather than a fixed count that's cramped or wastes
   space. */
.tag-index {
  column-width: 200px;
  column-gap: 28px;
}

.tag-index-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
  font-size: 13.5px;
  /* Keeps one tag's name+count together rather than splitting across the
     column break — the single biggest thing that makes a CSS-columns list
     look broken if left to the default. */
  break-inside: avoid;
}

.tag-index-item:hover .tag-index-name {
  text-decoration: underline;
}

.tag-index-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.tag-index-count {
  flex: 0 0 auto;
  color: var(--text-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* ---- bookmark permalink page (/bookmark/:id) ---- */

/* Same 640px column and centering as .bm-card below, so the two read as
   stacked rows rather than a toolbar off on its own — "Back to browse" on
   the left (where you came from), "Random" on the right (where to go
   next), both above the fold regardless of how tall the card's own
   thumbnail/description/tags turn out to be. */
.bm-topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: 640px;
  margin: 16px auto 0;
}

/* .tag-banner is shared with /tag/NAME (browse.html), which is full-width
   within its own container — scoped to just after .bm-topbar so it picks
   up this page's narrower 640px column instead of widening unexpectedly. */
.bm-topbar + .tag-banner {
  max-width: 640px;
  margin: 12px auto 0;
}

.bm-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 24px;
  width: 100%; /* see .ad-slot's comment above — same flex-shrink-wrap fix, needed here too since #videoCard reuses this class. Safe with padding: styles.css's global `* { box-sizing: border-box }` already applies here. */
  margin: 16px auto 24px;
  max-width: 640px;
}

.bm-image {
  width: 100%;
  max-height: 320px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid var(--border);
  margin-bottom: 16px;
}

/* No real max-height to derive from (the placeholder isn't an <img>, so it
   has no natural aspect ratio) — a fixed height stands in for one, smaller
   than .bm-image's own max-height since a big empty icon reads as more
   "broken" than a smaller, quieter one would. */
.bm-image-placeholder {
  height: 160px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent-soft);
  color: var(--text-muted);
}

/* Same cascade gotcha and fix as .row-thumb-placeholder[hidden] above —
   this element's own `display: flex` would otherwise beat the browser's
   default `[hidden]{display:none}` and defeat the broken-image fallback
   toggle in lib/publicPages.js's renderBookmarkPage. */
.bm-image-placeholder[hidden] {
  display: none;
}

.bm-image-placeholder svg {
  width: 48px;
  height: 48px;
}

.bm-title {
  font-size: 22px;
  margin: 0 0 10px;
}

.bm-url-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}

.bm-favicon {
  width: 16px;
  height: 16px;
  border-radius: 3px;
  flex: 0 0 auto;
}

/* The bookmark permalink page's one deliberate way to open a new tab — a
   real link straight to the external page (see lib/publicPages.js's own
   comment on thumb, just above where this is used, for the full picture of
   why only this one thing on that page opens a new tab). Accent-colored
   like any other link, since it is one there. */
.bm-url {
  color: var(--accent);
  font-size: 13.5px;
  word-break: break-all;
}

/* The toolbar viewer's own copy of this same card (public/frame.js's
   buildVideoCardHtml) renders bm-url as plain text, not a link — that
   page's one new-tab opener is its own "Open in a tab" button up in the
   toolbar, not a second link buried in this card. Overridden back to muted,
   non-link styling specifically inside #videoCard so it doesn't read as
   clickable there, without touching the real link .bm-url still is on the
   permalink page above. */
#videoCard .bm-url {
  color: var(--text-muted);
}

.bm-sitename {
  font-size: 12px;
  color: var(--text-muted);
  background: var(--accent-soft);
  border-radius: 6px;
  padding: 2px 8px;
}

.bm-description {
  font-size: 14.5px;
  color: var(--text);
  line-height: 1.5;
  margin: 0 0 14px;
}

.bm-tags {
  margin-bottom: 14px;
}

.bm-meta {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 10px;
}

.bm-notfound {
  text-align: center;
  padding: 48px 24px;
}

.bm-notfound h1 {
  font-size: 20px;
  margin: 0 0 8px;
}

.bm-notfound p {
  color: var(--text-muted);
  margin: 0 0 20px;
}

/* ---- comments (public/comments.js) — the bookmark permalink page
   (.comments-section sits right after .bm-card, both server-rendered, see
   lib/publicPages.js) and the toolbar viewer's video-embed mode
   (.comments-section is built client-side after #videoCard, see frame.js).
   Same 640px column as .bm-card above in both places, so it reads as a
   continuation of the card rather than a separate, differently-proportioned
   block. */
.comments-section {
  width: 100%; /* see .ad-slot's comment above — same flex-shrink-wrap fix */
  max-width: 640px;
  margin: 0 auto 24px;
}

.comments-heading {
  font-size: 16px;
  margin: 0 0 12px;
}

.comments-loading {
  color: var(--text-muted);
  font-size: 13px;
}

.comments-login-prompt {
  color: var(--text-muted);
  font-size: 13.5px;
  margin: 0 0 16px;
}

.comment-form {
  margin-bottom: 20px;
}

.comment-input {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  resize: vertical;
}

.comment-input:focus {
  outline: none;
  border-color: var(--accent);
}

.comment-form-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 8px;
}

.comment-form-error {
  color: #d33;
  font-size: 12.5px;
}

.comment-row {
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

.comment-row:first-child {
  border-top: none;
  padding-top: 0;
}

.comment-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-bottom: 4px;
}

.comment-author {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}

.comment-author:hover {
  text-decoration: underline;
}

.comment-date {
  font-size: 12px;
  color: var(--text-muted);
}

.comment-delete {
  margin-left: auto;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
  padding: 2px 4px;
}

.comment-delete:hover {
  color: #d33;
}

.comment-body {
  font-size: 14px;
  color: var(--text);
  line-height: 1.5;
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
