/* Page background -- a warmer gray than Bootstrap's own bg-body-secondary
   (#e9ecef, a cool blue-gray), behind the "container py-4" content area
   in base.html. Reads --site-bg-color (set by base.html's site_theme_vars/
   page_background_vars tags) so a content.PageBackground row for this
   page can override it, same as the plain `body` rule those tags target
   everywhere else -- #e8e4dd is only this class's own fallback, not a
   value those tags need to know about. */
.articles-page-bg {
  background-color: var(--site-bg-color, #e8e4dd);
}

/* The pill-shaped segmented tab switcher (.segmented-tabs*) used by
   article_list.html / learning_path_list.html and the author forms now
   lives in the shared static/shared/css/tabs.css, loaded from base.html
   -- extracted there once assessments became the third consumer, per
   agent_docs/frontend-css-conventions.md. */

/* The two-way catalogue switch row (.catalog-switch*, _catalog_switch.html)
   moved to static/shared/css/list-controls.css -- it's the flanking row that
   sits inside the shared .list-controls--sticky bar, now rendered on the
   assessments list + history too (assessments/_catalog_switch.html), not an
   articles-only pattern. Same move as .article-filter-* / .list-controls*
   before it, per agent_docs/frontend-css-conventions.md. */

/* The search/sort/filter toolbar (.list-controls*) and the category
   combobox styling (.article-filter-*) both now live in the shared
   static/shared/css/list-controls.css, loaded from base.html -- the
   combobox followed once the assessments list grew the same Filters
   panel. Per agent_docs/frontend-css-conventions.md. */

/* .add-card / .add-card__icon (the "create new" ghost tile) moved to
   static/shared/css/cards.css -- documents' catalogue was the 3rd app to
   need it, per agent_docs/frontend-css-conventions.md's "extract on the
   3rd" rule. */

/* Article card, shared by article_list.html's grid and
   article_detail.html's related-articles section (both include
   articles/_article_card.html). */

/* .card--lift is otherwise borderless (shadow-based elevation only); the
   light-gray outline is the shared .card--bordered modifier in
   static/shared/css/cards.css (also on billing's .pricing-card and
   assessments' .assessment-card). */

/* .card--lift lives in static/shared/css/cards.css (loaded from
   base.html) -- extracted there once a third app (documents) needed a
   byte-for-byte copy, per agent_docs/frontend-css-conventions.md's own
   "extract once a third app needs it" rule. position:relative on that
   shared rule is why the options menu (.article-card__options-menu,
   article_card_options.js) is deliberately *not* appended inside a
   card: :hover/:focus-within there sets a transform, which establishes
   a new stacking context that would trap a nested popover's z-index
   below whichever later sibling card happens to overlap it (this bit
   the old in-card version: opening the menu focuses a descendant, so
   :focus-within fired on every open). It's appended to <body> instead,
   positioned with real page coordinates. */

/* overflow:hidden + matching top corner radius live here rather than on
   .article-card itself -- clipping the *whole card* would also clip the
   share popover menu (appended lower down, inside .card-body) any time
   it renders taller than the remaining card height. Scoping the clip to
   just the image is enough: it's the only child that actually needs it
   (a plain rectangular <img>/fallback div poking past the card's rounded
   top corners), and .card-body has no background of its own to clip --
   it just shows the card's own already-rounded background through it. */
/* 0.5rem on every corner and 1.75rem under it, GitHub's resource cards
   (product owner, 2026-10-18). Nothing overlays the image any more (the
   labels moved under it, the icon stack is hidden), so the dark scrim
   that kept those overlays readable is gone too. The learning-path cards
   share this cover, their labels, title and footer with the article card
   (grouped selectors here; 2026-10-27 CSS audit): their cover is a
   *derived* article image (LearningPath.cover ->
   Article.featured_image_focal_style), and an uploaded override sets no
   focal custom properties and falls back to a plain centered crop. */
.article-card__image-wrap,
.learning-path-card__image-wrap {
  position: relative;
  overflow: hidden;
  border-radius: 0.5rem;
  margin-bottom: 1.75rem;
}

.article-card__image-link {
  display: block;
}

/* --focal-x/--focal-y/--zoom set inline per-article (Article.
   featured_image_focal_style) -- object-position centers the stored
   focal point in this box, then transform zooms in around that same
   point. Falls back to a plain centered, unzoomed crop (the previous
   behavior) wherever those custom properties aren't set, e.g. an image
   set via the admin without ever touching the crop/zoom editor. */
.article-card__image,
.learning-path-card__image {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: var(--focal-x, 50%) var(--focal-y, 50%);
  transform: scale(var(--zoom, 1));
  transform-origin: var(--focal-x, 50%) var(--focal-y, 50%);
}

.article-card__image--fallback,
.learning-path-card__image--fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #0067b8, #00365e);
  color: rgba(255, 255, 255, 0.85);
  font-size: 2.5rem;
}

/* "My learning paths" grid only (_library_paths_section.html) -- an
   enrolled member's card is grayscale until that article is read and
   full colour once it is (product owner, 2026-10-29; it was the other
   way round), driven initially by article.viewer_has_read
   (_article_card.html's own primary_learning_path check), and flipped
   live the moment a reader scrolls that same article to the end inside
   the read-in-a-modal feature (article_modal.js's own markCardRead(),
   articles:toggle_read, which removes this class). */
.article-card__image--grayscale {
  filter: grayscale(100%);
}

/* The labels row under the image: category, then Pinned when it applies
   (plain text, not filter links). A line just above it
   (.article-card__status) holds the publishing status on "My articles",
   or Read on a learning path's articles (article_modal.js adds that one
   live too). */
.article-card__status {
  display: flex;
  margin-bottom: 0.5rem;
}

.article-card__badges,
.learning-path-card__labels {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1.25rem;
}

/* .card__token (the neutral GitHub-style label) moved to
   static/shared/css/cards.css once the assessments catalogue card became
   its third app, after articles and learning paths. The colour modifiers
   below (Pinned, Read, status) only set the fill and text; this sheet
   loads after cards.css, so they win at equal specificity. */

/* .card__badge lives in static/shared/css/cards.css -- see the
   .card--lift comment above. Applied as an extra class alongside each
   specific badge class below, which supplies just the leftover
   background-color (and any modifier like uppercase). */

/* The category label is the plain neutral .card__token since the card's
   GitHub-style simplification (2026-10-18): it used to be a coloured pill
   over the photo, from branding.CardStyle's "Category badge" colours
   (Card Style Overrides in the admin), which no longer apply to article
   cards. The Pinned label below still takes that page's accent colour. */

/* Article.is_featured -- the "Pinned" label, so a pinned article, which
   silently overrides every sort order (articles/views.py's article_list),
   has a visible reason attached to it. A neutral .card__token like the
   category since 2026-10-18, with only its pin icon in the admin's accent
   colour (branding.CardStyle, Card Style Overrides): white text on an
   admin-picked fill couldn't promise 4.5:1 (the seeded #ef4136 is about
   3.9:1, WCAG 1.4.3), and the icon is decorative -- the word carries it. */
.article-card__pinned-badge i {
  color: var(--articles-card-accent-badge-color, #b92d5d);
}

/* Read/visited marker -- stacked under the category (and Pinned, if
   present) pill, only ever shown on a read article of the learning-path
   grid above (same primary_learning_path/viewer_is_enrolled/viewer_has_read
   gate, _article_card.html), so it's not wired into branding.CardStyle's
   admin-editable per-type colors like the two badges above -- a plain
   fixed green (not a taxonomy label an admin would want to retheme)
   reading as a simple "done" checkmark, matching this project's existing
   text-bg-success usage for the same "Completed"/"Enrolled" status
   badges elsewhere (_learning_paths_section.html, _library_paths_
   section.html). */
.article-card__read-badge,
.learning-path-card__completed-token {
  background-color: #198754;
  color: #fff;
}

/* Publishing-state marker -- "My articles" tab only (_article_card.html's
   show_owner_actions mode): Draft, Pending (scheduled), or Published.
   Fixed status colours, same reasoning as the read badge above: a status
   marker, not a taxonomy label an admin would want to retheme via
   branding.CardStyle. */
.article-card__status-badge--draft {
  background-color: #6c757d;
  color: #fff;
}

.article-card__status-badge--pending {
  background-color: #ffc107;
  color: #1d1d1f;
}

.article-card__status-badge--published {
  background-color: #198754;
  color: #fff;
}

/* The category line above the title on the article detail page
   (_article_detail_content.html): since 2026-10-19 it holds the same
   .card__token label as the cards (.article-card__category-badge), which
   sets its own 1rem size; this <p> stays as article_modal.js's hook. */
.article-card__category {
  font-size: 1.125rem;
}

/* Subcategory, inline next to the category -- on the article detail
   page's byline (article_detail.html, smaller than the surrounding
   category text) and inside .article-card__category-badge on cards
   (_article_card.html). Previously the card gave category a prominent
   colored pill on the image but buried subcategory as a separate,
   easy-to-miss muted text line lower in the card body -- same taxonomy
   level, wildly different visual weight. Folding it into the badge
   (as "CATEGORY · Subcategory", matching the detail page's own
   convention) keeps both readable at a glance in one place instead. */
.article-card__subcategory-inline {
  font-size: 0.875em;
}

/* Inside the badge pill, text is already small/bold/uppercase -- shrinking
   it further via the 0.75em above would make it illegible, so the badge
   context overrides back to full size and uses weight/opacity instead to
   stay visually secondary to the category name next to it. */
.article-card__category-badge .article-card__subcategory-inline {
  font-size: 1em;
}

/* The helpful / favorite / copy-link buttons (_article_side_icons.html)
   as a column. Rendered hidden on cards; on the article page
   .article-detail__actions below places them (they used to sit over the
   image's top-right corner, hence the absolute positioning here). */
.article-card__side-icons {
  position: absolute;
  top: 0.875rem;
  right: 0.875rem;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.375rem;
}

/* "Icon + count stacked" box -- used by .article-card__helpful-badge
   below (a read-only thumbs-up count; the card used to also have a
   favorite heart button here in the same shape, removed since). */
.article-card__count-icon {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.125rem;
  width: 1.6875rem;
  height: 2.625rem;
  padding: 0;
  border: none;
  border-radius: var(--bs-border-radius-sm, 0.25rem);
  background-color: rgba(233, 236, 239, 0.96);
  color: #3b3a39;
  overflow: hidden;
}

/* flex: 0 0 auto (not 1 1 50%, which used to force the icon to fill half
   the box and push it away from the count below) -- icon and count size
   to their own content and sit close together, centered as one tight
   group via the parent's gap above. */
.article-card__count-icon i {
  flex: 0 0 auto;
}

.article-card__count-label {
  flex: 0 0 auto;
  font-size: 0.5rem;
  font-weight: 600;
  line-height: 1;
  /* Fixed black rather than inheriting .article-card__count-icon's own
     color, so it stays legible/consistent regardless of what color the
     icon above it ends up using. */
  color: #000;
}

/* Helpful (thumbs-up) toggle -- shown to every visitor, logged in or
   not, same as the path badge below it. A second way (alongside the
   Yes/No prompt on the article detail page, articles/article_detail.html)
   to mark the same underlying vote -- both post to the same
   articles:submit_feedback endpoint, session-keyed, so clicking one and
   then the other is still just one vote, not two. article_feedback.js
   flips aria-pressed and adjusts the count optimistically once the POST
   actually succeeds, same pattern as article_favorite.js used to. */
/* Re-asserts the badge's own resting background on hover/focus/press --
   without this, Bootstrap's own .btn:hover/:active rules (this element
   also carries the plain .btn class) win over .article-card__count-icon's
   un-pseudo-classed background-color purely on specificity, resetting
   it to transparent and making the badge appear to vanish. :active needs
   the .article-card__side-icons ancestor prefix that :hover/:focus-visible
   don't -- Bootstrap's own `:not(.btn-check)+.btn:active` selector is
   three class-level selectors deep (specificity 0,3,0), so a plain
   `.article-card__helpful-badge:active` (0,2,0) still loses to it and
   the background keeps dropping to fully transparent for the
   mousedown-to-click window (reported as the badge "disappearing on
   click"); the extra ancestor class brings this rule to the same 0,3,0
   depth, so it wins on source order (this stylesheet loads after
   Bootstrap's). */
.article-card__helpful-badge:hover,
.article-card__helpful-badge:focus-visible,
.article-card__side-icons .article-card__helpful-badge:active {
  background-color: rgba(233, 236, 239, 0.96);
}

/* Only the icon glyph turns blue on hover/pressed -- the badge's own
   background/count-label stay exactly as at rest, unlike the article
   detail page's Yes/No buttons (which do fill solid). */
.article-card__helpful-badge:hover i,
.article-card__helpful-badge:focus-visible i,
.article-card__side-icons .article-card__helpful-badge:active i,
.article-card__helpful-badge[aria-pressed="true"] i {
  color: #0067b8;
}

/* Swaps the outline thumb for the filled one on hover/focus/pressed, same
   glyph-swap trick .article-favorite-btn used to for its heart -- \f406 is
   bootstrap-icons' bi-hand-thumbs-up-fill codepoint (admin_theme/static/
   admin_theme/vendor/bootstrap-icons). [aria-pressed="true"] covers this
   whether it's set server-side (article.viewer_marked_helpful, already
   voted on page load) or client-side (article_feedback.js, right after a
   click), so the filled state isn't just a transient hover preview. */
.article-card__helpful-badge:hover i.bi-hand-thumbs-up::before,
.article-card__helpful-badge:focus-visible i.bi-hand-thumbs-up::before,
.article-card__helpful-badge[aria-pressed="true"] i.bi-hand-thumbs-up::before {
  content: "\f406";
}

/* Favorite (heart) toggle -- sits directly below the
   helpful badge in .article-card__side-icons. A dedicated shortcut for
   the same toggle_favorite action the "..." options menu's own "Add to
   favorites" item already offers (article_card_options.js wires both to the same
   fetch-then-reflect-the-real-result toggle, and dispatches the same
   articles:favorite-toggled event my_library.js listens for). Single
   icon, no count, so it's a square rather than the icon+count
   .article-card__count-icon shape above.

   .article-card__copy-link-badge (the bi-link-45deg shortcut at the
   bottom of the stack) shares this exact shape and the hover/focus/press
   treatment below -- it's an action, not a toggle, so it has no
   [aria-pressed] "lit" state and no glyph swap. */
.article-card__favorite-badge,
.article-card__copy-link-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.6875rem;
  height: 1.6875rem;
  padding: 0;
  border: none;
  border-radius: var(--bs-border-radius-sm, 0.25rem);
  background-color: rgba(233, 236, 239, 0.96);
  color: #3b3a39;
}

/* Same re-assert-on-hover/focus/press fix as .article-card__helpful-badge
   above -- Bootstrap's .btn:hover/:active otherwise win on specificity
   and reset the background to transparent. :active needs the
   .article-card__side-icons ancestor prefix to match Bootstrap's own
   0,3,0-specificity `:not(.btn-check)+.btn:active` selector; see the
   helpful badge's comment above for the full reasoning. */
.article-card__favorite-badge:hover,
.article-card__favorite-badge:focus-visible,
.article-card__side-icons .article-card__favorite-badge:active,
.article-card__copy-link-badge:hover,
.article-card__copy-link-badge:focus-visible,
.article-card__side-icons .article-card__copy-link-badge:active {
  background-color: rgba(233, 236, 239, 0.96);
}

/* The copy-link glyph turns blue on hover/press, same treatment as
   the helpful badge's thumb. */
.article-card__copy-link-badge:hover i,
.article-card__copy-link-badge:focus-visible i,
.article-card__side-icons .article-card__copy-link-badge:active i {
  color: #0067b8;
}

/* The favorite heart glyph fills red once it's filled -- the hover/focus
   preview and the persistent favorited state ([aria-pressed="true"],
   set server-side from article.viewer_has_favorited or client-side by
   article_card_options.js) alike. Red, not the site accent blue: it's
   the conventional "liked / saved" colour and it stands apart from the
   copy-link and helpful glyphs beside it. */
.article-card__favorite-badge:hover i,
.article-card__favorite-badge:focus-visible i,
.article-card__side-icons .article-card__favorite-badge:active i,
.article-card__favorite-badge[aria-pressed="true"] i {
  color: var(--bs-danger, #dc3545);
}

/* Swaps the outline heart for the filled one on hover/focus/pressed --
   \f415 is bootstrap-icons' bi-heart-fill codepoint (admin_theme/
   static/admin_theme/vendor/bootstrap-icons), same glyph-swap trick as
   the helpful badge's thumb. [aria-pressed="true"] covers this whether
   it's set server-side (article.viewer_has_favorited, already set on
   page load) or client-side (article_card_options.js, right after a
   click), so the filled state isn't just a transient hover preview. */
.article-card__favorite-badge:hover i.bi-heart::before,
.article-card__favorite-badge:focus-visible i.bi-heart::before,
.article-card__favorite-badge[aria-pressed="true"] i.bi-heart::before {
  content: "\f415";
}

/* Edit / Delete -- icon-only owner actions in this same top-right
   corner stack (_article_side_icons.html's show_manage_icons block),
   replacing the old footer Edit/Delete row. Same opaque-pill-on-photo
   shape as .article-card__favorite-badge/.article-card__copy-link-badge
   just above -- mirrors documents/_template_card_cols.html's own
   top-right "Edit" corner icon (documents.css's
   .document-card__corner-icon-btn) in spirit/placement, but reuses this
   stack's own treatment rather than that navy card's ghost-circle-on-
   dark-surface one, since a photo underneath can be any colour. */
.article-card__edit-badge,
.article-card__delete-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.6875rem;
  height: 1.6875rem;
  padding: 0;
  border: none;
  border-radius: var(--bs-border-radius-sm, 0.25rem);
  background-color: rgba(233, 236, 239, 0.96);
  color: #3b3a39;
  text-decoration: none;
}

/* Same re-assert-on-hover/focus fix as .article-card__favorite-badge
   above -- Bootstrap's .btn:hover otherwise wins on specificity and
   resets the background to transparent. */
.article-card__edit-badge:hover,
.article-card__edit-badge:focus-visible {
  background-color: rgba(233, 236, 239, 1);
  color: #0067b8;
}

.article-card__delete-badge:hover,
.article-card__delete-badge:focus-visible {
  background-color: rgba(233, 236, 239, 1);
  color: var(--bs-danger, #dc3545);
}

/* Per-badge positioning context for the toast bubble below -- every
   interactive side-icon badge (helpful, favorite, copy-link) is wrapped
   in one of these so its own .article-card__icon-toast can anchor to it
   (via right:100%) regardless of where it sits in the stack. */
.article-card__icon-toast-wrap {
  position: relative;
}

/* Short click-confirmation bubble shown briefly to the left of a
   just-clicked side-icon badge -- "You found this article helpful",
   "Article link copied to clipboard", "Added to favorites", etc.
   (article_feedback.js / article_card_options.js set the text, un-hide
   it, then hide it again on a timer). The badges have no persistent
   visible label to repurpose for this, unlike the "..." menu's own
   items which swap their label text the same way. z-index:3 to sit above
   the image scrim (1) and badges (2). */
.article-card__icon-toast {
  position: absolute;
  right: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-right: 0.5rem;
  z-index: 3;
  padding: 0.25rem 0.75rem;
  border-radius: var(--bs-border-radius-pill, 50rem);
  background-color: rgba(0, 0, 0, 0.75);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
}

.article-card__icon-toast[hidden] {
  display: none;
}

/* Opens the card's own options menu (Add to favorites / Copy article
   link / Report article, article_card_options.js) -- sits beside the
   title on the card face (_article_card.html). Same button/class reused
   verbatim on the article detail page and "My learning paths" modal in
   place of a dedicated Share button (article_detail.html/
   _article_detail_content.html). Ghost icon button so it doesn't
   compete with whatever it sits next to. */
.article-card__more-btn {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: none;
  border-radius: var(--bs-border-radius-sm, 0.25rem);
  background-color: transparent;
  color: inherit;
}

/* Weight/clamp match documents.css's .document-card__illustrated-title
   (the /templates/?view=all catalogue card's own title, same
   row-cols-1/md-2/lg-3 grid width as this card) -- colour stays this
   card's own default (not that card's white-on-navy) since this card
   sits on a light background, not a solid navy surface. font-size is
   dialed back from that card's own 2.25rem (too large for this card's
   meta-line-led layout) while staying well above Bootstrap's plain h5. */
/* GitHub's card heading: 22px on a 1.4 line, medium weight, in the
   heading colour rather than link blue (the whole card is the link; its
   hover cue is the "Read article" arrow). */
.article-card__title,
.learning-path-card__title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-bottom: 0.75rem;
  font-size: 1.375rem;
  font-weight: 500;
  line-height: 1.4;
}

.article-card__title a {
  color: var(--site-heading-color, #1d1d1f);
}

/* Tighter than Bootstrap's default .card-body padding (1rem top/bottom,
   --bs-card-spacer-y) -- the title+"..." row is now card-body's first
   child, and the full 1rem left too much air between the image above and
   it. Only padding-top is trimmed; the other three sides read from
   --site-card-padding (branding.SiteCardStyles) instead, and padding-top
   still wins after it by source order within this same rule. */
/* No border, fill, shadow or padding: the card is its content on the page,
   GitHub-style (product owner, 2026-10-18, an exception to the bordered,
   lifted cards elsewhere). 0.5rem corners match the image's. */
.article-card {
  --bs-card-border-width: 0;
  --bs-card-bg: transparent;
  border-radius: 0.5rem;
}

.article-card .card-body {
  padding: 0;
}

/* The card's teaser (Article.card_teaser: the excerpt, else the
   subtitle; left out when neither is set). Both are author-written and
   short, so this rarely clamps; -webkit-line-clamp is the safety net for
   a long one, putting the "…" at the exact visual cutoff point.
   line-height/max-height pinned explicitly (2 lines * line-height) as a
   belt-and-suspenders against -webkit-line-clamp's own known rendering
   quirk -- without an exact height match, a sliver of an extra line can
   peek through below the clamp/ellipsis in some browsers. Font-size/
   line-height/line-count match documents.css's own
   .document-card__illustrated-teaser (that card's own description copy,
   same grid width as this card). */
.article-card__summary {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 0.75rem;
  color: #5c6672;
  font-size: 1rem;
  line-height: 1.5;
  max-height: 6em;
}

/* All of Article.tags (capped at 5 by Article.clean()), as a one-line
   horizontal scroll strip rather than a wrapping row, so every card's tag
   line is one fixed height. Every tag is a real, focusable <a>, and the
   page itself still only scrolls vertically (WCAG 1.4.10). Scrolling is
   owned by article_card_tags_scroll.js (touch-action: none hands it the
   whole gesture); min-width: 0 lets this flex item shrink below its
   content so overflow-x has somewhere to move. */
.article-card__tags {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  touch-action: none;
  cursor: grab;
  gap: 0.375rem;
  padding: 0.125rem 0.125rem 0.1875rem;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.article-card__tags:active {
  cursor: grabbing;
}

.article-card__tags::-webkit-scrollbar {
  display: none;
}

/* Flanking the strip, far left/far right of the same row -- each starts
   `hidden` in the markup and article_card_tags_scroll.js only un-hides
   the end(s) that still have somewhere to scroll to (both stay hidden
   when a card's tags don't overflow at all; a no-JS visit never
   un-hides either, same reasoning as the strip itself staying
   swipeable/scrollable on its own regardless). flex-shrink:0 so a long
   tag strip squeezes the scroller, never these buttons. */

.article-card__tag-badge {
  display: inline-block;
  flex-shrink: 0;
  white-space: nowrap;
  /* A swipe that starts right on top of a pill (not the gap beside it)
     otherwise risks the browser's native link-drag-and-drop starting
     instead of a plain scroll/tap -- harmless on a real touchscreen
     (which dispatches Touch events, not this), but a touchscreen laptop
     or stylus that dispatches pointer/mouse events for a "swipe" can
     trigger it, confirmed while testing this strip's scroll behaviour.
     A tag chip has no reason to be draggable in the first place. */
  -webkit-user-drag: none;
  user-select: none;
  font-size: 0.8125rem;
  color: #495057;
  /* #6c757d, not the lighter #ced4da other borders on this page use --
     a pill's own boundary needs WCAG 1.4.11's 3:1 non-text contrast
     against the white card background to read as a clickable control
     rather than plain text (#ced4da only hits ~1.5:1); matches
     .article-detail__tag-badge's border color below for the same
     reason. */
  border: 1px solid #6c757d;
  padding: 0.15rem 0.5rem;
  border-radius: 980px;
  text-decoration: none;
}

.article-card__tag-badge:hover,
.article-card__tag-badge:focus-visible {
  border-color: #495057;
  color: #212529;
}

.article-card__read-actions,
.article-card__more-btn,
.article-card__tag-badge,
.article-card__remove-from-library,
.article-card__owner-actions {
  position: relative;
  z-index: 2;
}

/* "Read" CTA -- the same big pill-button treatment as documents.css's
   .document-card__pill-btn (and its --primary variant, tuned for a
   light-background card like this one rather than that card's solid
   navy surface), duplicated under this app's own class name per
   agent_docs/frontend-css-conventions.md ("defined once per app
   stylesheet... until a 3rd app needs it, then promote to
   static/shared/css/cards.css") -- documents is the only other app
   using this shape so far. Blue (this site's own primary accent),
   not documents' gold default. Not Bootstrap's .btn (reset to the
   theme's own blue/gray button shapes) -- a bare <a> styled from
   scratch, same reasoning as the pill it's copied from. */
.article-card__read-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 1.4rem;
  border: 0;
  border-radius: 980px;
  background-color: var(--site-btn-primary-bg-color, #0067b8);
  color: #fff;
  font-size: 0.9375rem;
  font-weight: 700;
  text-decoration: none;
}

.article-card__read-btn:hover,
.article-card__read-btn:focus-visible {
  background-color: #00558f;
  color: #fff;
}

.article-card__read-btn:focus-visible {
  outline: 3px solid #16233c;
  outline-offset: 2px;
}

/* .btn-pill itself moved to static/shared/css/cards.css -- assessments'
   builder Overview step and documents' builder Settings step both
   reached for it too (reported directly: match article_author_form.html's
   own "Edit image" button everywhere else with the same feature),
   which is the "promote on a 3rd app" trigger agent_docs/
   frontend-css-conventions.md documents; see that file's own comment. */

/* The Favorites page's Articles tab only -- see _article_card.html's
   own show_remove_from_library comment. Fades out (my_library.js) rather
   than disappearing instantly when removed, so the change reads as
   deliberate rather than the page glitching. */
.article-card__remove-from-library {
  white-space: nowrap;
}

.article-card--removing {
  transition: opacity 0.2s ease;
  opacity: 0;
}

/* Dedicated empty spacer (_article_card.html, between .article-card__summary
   and the optional .article-card__tags) rather than margin-top: auto on
   the divider below -- an auto margin there absorbed the leftover space
   *directly above the divider*, which pinned the footer to the bottom
   correctly but let the gap open up between the tags and the divider
   instead of between the summary and the tags, so tags landed at a
   different height on every card depending on title/summary length.
   flex-grow here instead absorbs that same leftover space *before* the
   tags, so tags (when present) always sit a fixed distance above the
   divider, and the footer still lands flush with the card bottom either
   way (with or without a tags row) since this spacer exists on every
   card regardless. Generic name (agent_docs/frontend-css-conventions.md)
   -- no other card uses this yet, but it's a plain generic flex-spacer
   pattern any future card could reuse. */
.card__spacer {
  flex: 1 1 auto;
}

/* .card__divider lives in static/shared/css/cards.css -- see the
   .card--lift comment above. */

/* The footer: just the card's actions (Read article, or Edit / Delete on
   "My articles", or Remove from favorites on Favorites); the read time
   sits under the summary. */
.article-card__footer,
.learning-path-card__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
}

.article-card__read-actions,
.article-card__owner-actions {
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.article-card__owner-actions form {
  display: contents;
}

/* Text links (and Delete, a text-style button), not pill buttons -- on the
   learning-path reader card too (.learning-path-card__link: Preview /
   Resume / Enroll / Unenroll). The vertical padding keeps each target at
   least 24px tall (WCAG 2.5.8). */
.article-card__read-link,
.article-card__edit-btn,
.article-card__delete-btn,
.learning-path-card__link {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0;
  border: 0;
  background: none;
  color: var(--site-link-color, #0067b8);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.5;
  text-decoration: none;
}

.article-card__read-link:hover,
.article-card__edit-btn:hover,
.article-card__delete-btn:hover,
.learning-path-card__link:hover {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Bootstrap's danger-text-emphasis red, 6.4:1 on white. Unenroll on a
   learning-path card takes it too (learning_path_enroll.js keeps
   data-is-enrolled current). */
.article-card__delete-btn,
.learning-path-card__enroll-btn[data-is-enrolled="true"] {
  color: #b02a37;
}

/* GitHub's expanding arrow on "Learn more": a chevron whose stem draws in
   (stroke-dashoffset) as it nudges 2px right, while the pointer is over
   the card or focus is inside it. */
.article-card__read-arrow {
  flex-shrink: 0;
  margin-inline-start: 0.25rem;
  transition: transform 0.2s ease;
}

.article-card__read-arrow-stem {
  stroke-dasharray: 10;
  stroke-dashoffset: 10;
  transition: stroke-dashoffset 0.2s ease;
}

.article-card:hover .article-card__read-arrow,
.article-card:focus-within .article-card__read-arrow,
.learning-path-card[data-lp-card]:hover .article-card__read-arrow,
.learning-path-card[data-lp-card]:focus-within .article-card__read-arrow {
  transform: translateX(2px);
}

.article-card:hover .article-card__read-arrow-stem,
.article-card:focus-within .article-card__read-arrow-stem,
.learning-path-card[data-lp-card]:hover .article-card__read-arrow-stem,
.learning-path-card[data-lp-card]:focus-within .article-card__read-arrow-stem {
  stroke-dashoffset: 20;
}

@media (prefers-reduced-motion: reduce) {
  .article-card__read-arrow,
  .article-card__read-arrow-stem {
    transition: none;
  }
}

/* Under the summary, with the summary's old 1.5rem before the footer. */
.article-card__read-time {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 0.25rem;
  margin-bottom: 1.5rem;
  white-space: nowrap;
}

/* The category-filter combobox styling (.article-filter-category-input /
   -subcategory-input / -category-menu) moved to
   static/shared/css/list-controls.css -- it's part of the shared Filters
   panel (_filter_sidebar.html), now rendered on the assessments list too,
   not an articles-only pattern. */

/* The article-picker search box -- a leading bi-search icon inside a
   Bootstrap .input-group, boxed the same way as this page's other full-box
   fields (.article-form-select below) rather than a bottom-only rule --
   see that rule's own comment for the accessibility rationale (a
   bottom-only underline reads as an input boundary only once you already
   know to look for it). The border is the site-wide field border
   (static/shared/css/fields.css), like .article-form-select /
   .scheduled-publish-widget's own boxed fields.
   Public form only (articles.css) -- the admin keeps its own themed
   .form-control here. */
.learning-path-articles-search {
  margin-top: 0.5rem;
}

.learning-path-articles-search .input-group-text,
.learning-path-articles-input {
  background-color: transparent;
  border-color: var(--site-input-border-color, #767676);
}

/* A full ring around the whole box on focus, not just the input's own
   edge -- same pattern as .article-form-select:focus below (WCAG 2.4.7:
   the entire control's focus state should be visible, not implied by one
   side changing). border turns transparent instead of disappearing so
   the box doesn't resize at focus. */
.learning-path-articles-search:focus-within {
  border-radius: var(--bs-border-radius, 0.375rem);
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8);
}

.learning-path-articles-search:focus-within .input-group-text,
.learning-path-articles-search:focus-within .learning-path-articles-input {
  border-color: transparent;
}

.learning-path-articles-input:focus {
  background-color: transparent;
  box-shadow: none;
}

/* "Only show templates in this article's category" toggle
   (practice_templates.html, PracticeTemplatesWidget only -- the
   learning-path article picker has no such filter). Bootstrap's own
   .form-check relies on padding-left + an absolutely-positioned input to
   line the switch up with the label's first line of text -- both are
   tuned for the *default* track size, so bumping the track up to 3em
   below throws that off. A flex row sidesteps it entirely (gap instead
   of padding/absolute-position math) and keeps the switch vertically
   centered with the label regardless of track size; margin above/below
   gives the row breathing room from the empty-state hint above it and
   the search box below. */
[data-practice-templates] .form-check.form-switch {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding-left: 0;
  margin: 1rem 0;
}

[data-practice-templates] .form-check.form-switch .form-check-input {
  float: none;
  margin: 0;
}

/* Sized up from Bootstrap's own small default switch track, same
   explicit-size convention as list-controls.css's own
   .list-controls__filter-switch. */
[data-practice-templates] .form-switch .form-check-input {
  width: 3em;
  height: 1.5em;
  border-radius: 2em;
}

/* Bootstrap's own default border (rgba(0,0,0,.25)) all but disappears
   against this page's warm-cream background at this track size --
   darkened to this page's other boxed-field border colour (.article-
   form-select etc.). :not(:checked) only -- the checked state already
   gets a clearly visible accent-colour fill + border from Bootstrap, no
   help needed there. */
[data-practice-templates] .form-switch .form-check-input:not(:checked) {
  border-color: #767676;
}

/* The added-articles list (learning_path_articles.js builds
   .list-group-item rows into [data-role="list"]) -- drop Bootstrap's own
   white .list-group-item fill so the rows sit flush on the page's warm-
   cream background, same "remove white background" pass as the rest of
   this form, and darken the row dividers (Bootstrap's default ~#dee2e6
   all but vanishes on the cream ground) to this form's own #767676
   border colour. Public form only (articles.css) -- the admin keeps its
   themed list-group. */
.learning-path-articles [data-role="list"] .list-group-item {
  background-color: transparent;
  border-color: #767676;
}

/* The up/down reorder buttons render as .btn-outline-secondary, which
   base.html's SiteTheme override fills solid white (--bs-btn-bg) -- reset
   to transparent so they sit flush on the page like the plain
   .btn-outline-danger "x" beside them. Hover/focus/active fills are left
   as the theme set them. */
.learning-path-articles [data-role="list"] .btn-outline-secondary {
  --bs-btn-bg: transparent;
}

/* Drag handle + native HTML5 drag-and-drop states for the added-items
   list (learning_path_articles.js / practice_templates.js -- both share
   this markup, kept in step per this file's own "shared widget shape"
   convention). The up/down buttons stay the primary reorder control
   (keyboard + touch); dragging is a mouse/trackpad-only shortcut on top
   of them, not a replacement. */
.learning-path-articles-drag-handle {
  cursor: grab;
  color: #9aa5b1;
  padding-right: 0.25rem;
  line-height: 1;
}

.learning-path-articles [data-role="list"] .list-group-item.is-dragging {
  opacity: 0.5;
}

.learning-path-articles [data-role="list"] .list-group-item.is-drag-over {
  border-top: 2px solid #0d6efd;
}

/* Learning-path author form's article picker (learning_path_articles.js /
   articles/widgets.py's LearningPathArticlesWidget). Same dropdown visual
   language as .article-filter-category-menu above -- the admin has its
   own copy of these rules in admin_theme's admin-overrides.css, themed
   with that skin's own vars; keep the two in step (same convention as
   the card badges). */
.learning-path-articles-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 10;
  max-height: 260px;
  overflow-y: auto;
  margin: 0.25rem 0 0;
  padding: 0.25rem 0;
  list-style: none;
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 0.5rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  font-size: 0.8125rem;
}

.learning-path-articles-menu.show {
  display: block;
}

.learning-path-articles-menu li[role="option"] {
  padding: 0.4375rem 0.75rem;
  cursor: pointer;
}

.learning-path-articles-menu li[role="option"]:hover,
.learning-path-articles-menu li[role="option"].is-active {
  background-color: #f2f2f2;
}

.learning-path-articles-menu__message {
  padding: 0.4375rem 0.75rem;
  color: #6c757d;
  font-style: italic;
}

/* The reading column: 800px, centred by mx-auto in the markup.
   Positioning context for the action buttons' sticky column
   (.article-detail__actions below). The learning path page's sections
   share the same column width. */
.article-detail {
  position: relative;
  max-width: 800px;
}

.learning-path-detail,
.learning-path-progress {
  max-width: 800px;
}

/* The article's action buttons (_article_side_icons.html: helpful,
   favorite, copy link), out of the hero image since 2026-10-19 (product
   owner). By default a row on the author line, beside the "..." button
   (phones, tablets, and the reading modals). Their confirmation bubbles
   open under the author line at its right edge (the line is their
   positioning context here), so they never run off a phone's left edge
   when the row wraps under the author details. 1rem resets the author
   line's .small so the icons keep their size. */
.article-detail__byline {
  position: relative;
}

.article-detail__actions {
  font-size: 1rem;
}

.article-detail__actions .article-card__side-icons {
  position: static;
  flex-direction: row;
  align-items: center;
}

/* In the row the helpful count sits to the right of the thumb, so the
   button is as tall as the heart and link beside it (product owner,
   2026-10-19). The column below keeps it stacked. */
.article-detail__actions .article-card__count-icon {
  flex-direction: row;
  gap: 0.25rem;
  width: auto;
  min-width: 1.6875rem;
  height: 1.6875rem;
  padding: 0 0.375rem;
}

.article-detail__actions .article-card__count-label {
  font-size: 0.75rem;
}

.article-detail__actions .article-card__icon-toast-wrap {
  position: static;
}

.article-detail__actions .article-card__icon-toast {
  top: 100%;
  right: 0;
  margin-top: 0.375rem;
  margin-right: 0;
  transform: none;
}

/* Full page from 992px, where the 800px column leaves room beside it
   (66px at 992px, 210px at 1280px): a column just right of the article,
   from its top, that stays in view while reading. The outer box spans the
   whole article (top/bottom 0, positioned against .article-detail) so the
   inner stack can stick under the navbar for the full length of it;
   --scroll-clear-top is the sticky navbar's live height plus a breather
   (static/shared/js/scroll_clearance.js). The author line stops being a
   positioning context so the column measures against the whole article,
   and each bubble opens to the left of its own button again, over the
   article's edge. */
@media (min-width: 992px) {
  .article-detail--page .article-detail__byline {
    position: static;
  }

  .article-detail--page .article-detail__main {
    position: relative;
  }

  .article-detail--page .article-detail__actions .article-card__icon-toast-wrap {
    position: relative;
  }

  .article-detail--page .article-detail__actions {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 100%;
    margin-left: 1rem;
  }

  .article-detail--page .article-detail__actions .article-card__side-icons {
    position: sticky;
    top: var(--scroll-clear-top, 5.25rem);
    flex-direction: column;
  }

  .article-detail--page .article-detail__actions .article-card__count-icon {
    flex-direction: column;
    gap: 0.125rem;
    width: 1.6875rem;
    height: 2.625rem;
    padding: 0;
  }

  .article-detail--page .article-detail__actions .article-card__count-label {
    font-size: 0.5rem;
  }

  .article-detail--page .article-detail__actions .article-card__icon-toast {
    top: 50%;
    right: 100%;
    margin-top: 0;
    margin-right: 0.5rem;
    transform: translateY(-50%);
  }
}

/* Detail page hero image / fallback -- overflow:hidden lives on the wrap
   (not the image itself) since transform: scale() below needs an
   ancestor clipping box to actually crop the zoomed-in image rather
   than have it visually spill over the surrounding page content; same
   split .article-card__image-wrap/.article-card__image already uses. */
.article-detail__hero-wrap {
  overflow: hidden;
  border-radius: 0.5rem;
}

.article-detail__hero {
  display: block;
  width: 100%;
  aspect-ratio: 1920 / 1080;
  object-fit: cover;
  object-position: var(--focal-x, 50%) var(--focal-y, 50%);
  transform: scale(var(--zoom, 1));
  transform-origin: var(--focal-x, 50%) var(--focal-y, 50%);
}

.article-detail__hero--fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 6;
  background: linear-gradient(135deg, #0067b8, #00365e);
  color: rgba(255, 255, 255, 0.85);
  font-size: 4rem;
  border-radius: 0.5rem;
}

/* Smaller than Bootstrap's own .small (0.875em) -- a photo credit is a
   minor attribution line, not body-adjacent text competing for
   attention. Generic name (agent_docs/frontend-css-conventions.md) --
   used under both the article detail hero (_article_detail_content.html)
   and the learning-path detail cover (learning_path_detail.html). */
.card__image-credit {
  font-size: 0.75rem;
}

/* Article.body_html (article_detail.html) is real HTML rendered from the
   stored Quill Delta, otherwise left at Bootstrap's plain 1rem/1.5 body
   defaults -- a touch larger and
   noticeably more open here for comfortable long-form reading in an
   ~800px column (the <article>'s own max-width), plus breathing room
   above any subheadings the author added within the body so they read
   as section breaks rather than sitting flush against the prior
   paragraph. */
.article-detail__body {
  font-size: 1.0625rem;
  line-height: 1.7;
}

.article-detail__body :is(h2, h3, h4) {
  margin-top: 2rem;
}

/* Card options popover (article_card_options.js, opened by
   .article-card__more-btn -- the meta line on every card, and now this
   same button in place of a dedicated Share button on both the article
   detail page and the "My learning paths" modal, article_detail.html/
   _article_detail_content.html). Appended to <body> (not nested inside
   the card) with top/left set in page-coordinate pixels by
   article_card_options.js, rather than position:absolute against an
   in-card ancestor --
   .article-card:hover/:focus-within sets a transform, which establishes
   a new stacking context and would otherwise trap this menu's z-index
   below whichever *later* sibling card happens to visually overlap it.
   z-index 1080 (Bootstrap's own $zindex-popover) rather than a low
   single-digit value -- this same button/menu also opens inside the
   "My learning paths" read-in-a-modal feature, and Bootstrap's
   .modal/.modal-backdrop sit at 1055/1050, well above a low z-index --
   the menu would otherwise open successfully but render invisibly
   behind the modal itself. */
.article-card__options-menu {
  position: absolute;
  z-index: 1080;
  min-width: 200px;
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 0.5rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  padding: 0.5rem;
}

.article-card__options-menu button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  /* One line per item -- the menu widens to its longest label ("Remove
     from Saved learning paths") rather than wrapping it; positionMenu
     keeps the widened box on-screen. */
  white-space: nowrap;
  background: none;
  border: none;
  padding: 0.5rem 0.75rem;
  border-radius: 0.375rem;
  color: #212529;
}

/* Keep the icon column aligned when labels differ in length. */
.article-card__options-menu button .bi {
  flex: 0 0 auto;
}

.article-card__options-menu button:hover,
.article-card__options-menu button:focus-visible {
  background-color: #f1f3f5;
}

/* Article tags (article_detail.html) -- each links into the search
   results for that tag name (Article.tags participates in article_list's
   own search filter, articles/views.py), since there's no dedicated
   tag-filter UI yet (see ArticleTag's own docstring in articles/models.py). */
.article-detail__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.article-detail__tag-badge {
  display: inline-block;
  border: 1px solid #6c757d;
  color: #495057;
  font-size: 0.8125rem;
  padding: 0.25rem 0.75rem;
  border-radius: 980px;
  text-decoration: none;
}

.article-detail__tag-badge:hover,
.article-detail__tag-badge:focus-visible {
  border-color: #495057;
  color: #212529;
}

/* Learning Path progress bar (article_detail.html, under the "Learning
   outcome" blurb) -- how many of this path's own articles this reader
   has read (articles/views.py's article_detail, learning_path_read_count
   / learning_path_total_count) out of its total. background-color/border
   set explicitly rather than left to Bootstrap's own --bs-progress-bg
   (#e9ecef, close in lightness to this page's own warm #e8e4dd
   background -- articles-page-bg above) -- at 0% read, the fill
   (.progress-bar below) is 0 wide, so the *track* is the only thing
   distinguishing "an empty progress bar" from "no progress bar at all",
   and the two near-identical light grays made it disappear entirely
   against the page. */
.learning-path-progress .progress {
  height: 0.875rem;
  background-color: #dee2e6;
  border: 1px solid #ced4da;
}

.learning-path-progress .progress-bar {
  background-color: #0067b8;
}

/* "Congratulations" message (_library_paths_section.html's "My learning
   paths" rows, .learning-path-progress__congrats -- server-rendered,
   only present at all once row.progress_percent == 100; this page
   reloads rather than toggling it client-side, see learning_path_
   enroll.js's own docstring). Sized up from the surrounding small text so
   it still reads as a standout reward moment, in the text colour rather
   than link blue (product owner, 2026-10-19): bold blue read as a link.
   Only its leading "Congratulations!" (<strong>) is bold; "You completed
   this learning path on October 14, 2026." is normal weight. No 🎉. */
.learning-path-progress__congrats {
  font-size: 1.0625rem;
  color: var(--site-text-color, #1d1d1f);
}

/* learning_paths/detail.html's progress block is a normal section since
   2026-10-19 (product owner): no longer pinned under the navbar, and no
   border, padding or shadow -- just the progress line, bar and message. */

/* "This article is part of a learning path" banner
   (_article_detail_content.html, .article-detail__learning-path-banner)
   -- same brand blue as this page's other learning-path accents above
   (progress bar fill, "Congratulations" text) and the detail hero's own
   fallback gradient, so a solid-color prompt reads as part of the same
   visual family rather than a generic Bootstrap alert. */
.article-detail__learning-path-banner {
  background-color: #0067b8;
}

/* Related Insights carousel (article_detail.html, article_related_carousel.js)
   -- a horizontally-scrolling track rather than Bootstrap's own .carousel
   component: Bootstrap's carousel shows one full slide at a time, but
   this needs several cards visible at once (matching the old 3-per-row
   grid it replaced) while staying responsive across breakpoints without
   baking "N cards per slide" into server-rendered groups. scroll-snap
   gives native touch/trackpad/keyboard scrolling for free; the Prev/Next
   buttons (below) just nudge that same native scroll position by one
   card's width.

   A slim horizontal scrollbar is kept visible at rest: the OS overlay
   scrollbar (macOS "show scroll bars: when scrolling", and Chrome's own
   on some platforms) fades out when idle, so on a wide screen the cards
   past the third give no hint they exist -- only the Prev/Next chevrons
   do, and those read as optional. Styling the ::-webkit-scrollbar
   pseudo-elements also opts the track out of overlay behaviour in
   Blink/WebKit, so the bar shows whenever the track overflows; Firefox
   uses scrollbar-width/scrollbar-color. padding-bottom keeps the bar
   clear of the cards. */
.related-insights-carousel__track {
  display: flex;
  gap: 1.5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-bottom: 0.75rem;
  scrollbar-width: thin;
  scrollbar-color: #b6bcc2 transparent;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x;
}

.related-insights-carousel__track::-webkit-scrollbar {
  height: 8px;
}

.related-insights-carousel__track::-webkit-scrollbar-track {
  background-color: rgba(20, 20, 20, 0.08);
  border-radius: 100px;
}

.related-insights-carousel__track::-webkit-scrollbar-thumb {
  background-color: #b6bcc2;
  border-radius: 100px;
}

.related-insights-carousel__track::-webkit-scrollbar-thumb:hover {
  background-color: #969ca3;
}

/* Mouse click-and-drag panning (article_related_carousel.js). The grab
   cursor only appears when there's something to scroll to (the JS adds
   --draggable), and a bare grab-cursor on a link/text surface is
   deliberately kept subtle. */
.related-insights-carousel__track--draggable {
  cursor: grab;
}

/* Pointer drag-to-scroll ("swipe") state, toggled by
   article_related_carousel.js -- same look as documents.css's own
   .template-home__resume-grid.is-dragging / .template-home__tools.is-dragging
   (the /templates/ overview's horizontal rails), plus scroll-snap-type:
   none here specifically: this track (unlike those two rails) snaps to
   card boundaries at rest, and leaving mandatory snapping on *during* the
   drag fights every scrollLeft write article_related_carousel.js makes,
   producing a jarring jump instead of a smooth 1:1 follow. JS re-snaps
   with an explicit smooth scrollTo() once the drag ends. */
.related-insights-carousel__track.is-dragging {
  cursor: grabbing;
  scroll-behavior: auto;
  scroll-snap-type: none;
  user-select: none;
}

.related-insights-carousel__track.is-dragging .related-insights-carousel__item {
  pointer-events: none;
}

/* flex-basis matches the breakpoints/gutter of the row-cols-1
   row-cols-sm-2 row-cols-lg-3 g-4 grid this carousel replaced -- 1 card
   per view below 576px, 2 from 576px, 3 from 992px, each still full
   height (align-items: stretch is flex's own default, so every card in
   view stays the same height as its tallest sibling). */
.related-insights-carousel__item {
  flex: 0 0 100%;
  max-width: 100%;
  scroll-snap-align: start;
}

@media (min-width: 576px) {
  .related-insights-carousel__item {
    flex-basis: calc((100% - 1.5rem) / 2);
    max-width: calc((100% - 1.5rem) / 2);
  }
}

@media (min-width: 992px) {
  .related-insights-carousel__item {
    flex-basis: calc((100% - 3rem) / 3);
    max-width: calc((100% - 3rem) / 3);
  }
}

/* Four cards in view from 1200px (the home page's insights carousel,
   public/home/_insights.html, reuses this whole component and adds this
   modifier on its wrapper); below that the breakpoints above apply. The
   gap is the track's 1.5rem, three of them between four cards. */
@media (min-width: 1200px) {
  .related-insights-carousel--four .related-insights-carousel__item {
    flex-basis: calc((100% - 4.5rem) / 4);
    max-width: calc((100% - 4.5rem) / 4);
  }
}

.related-insights-carousel__controls {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}

.related-insights-carousel__prev:disabled,
.related-insights-carousel__next:disabled {
  opacity: 0.35;
}

/* .card--lift's own :hover/:focus-within lift (translateY(-4px) +
   shadow, near the top of this file) gets clipped here specifically --
   setting overflow-x: auto on .related-insights-carousel__track above
   implicitly computes its own overflow-y to auto too (a CSS overflow
   spec quirk: one axis non-"visible" forces the other off "visible" as
   well), so the track clips vertically as a scroll container now, and
   the lifted card's top edge gets cut off by that clip. Overriding the
   hover state back to a no-op here (rather than, say, adding track
   padding to make room for the lift) is the simplest fix that doesn't
   also change the track's scrollable width/snap points. */
.related-insights-carousel__item .card--lift:hover,
.related-insights-carousel__item .card--lift:focus-within,
.related-insights-carousel__item .article-card:hover,
.related-insights-carousel__item .article-card:focus-within {
  transform: none;
  box-shadow: none;
}

/* article_author_form.html's Publishing tab -- ScheduledPublishWidget's
   hour/minute/AM-PM selects (articles/widgets.py), sat in a .d-flex row
   (articles/widgets/scheduled_publish.html) with no explicit sizing,
   otherwise shrink enough that their own selected option text gets
   clipped. min-width comfortably fits the widest option in any of the
   three ("12", "55", "PM"); flex-wrap on the row (rather than also
   setting flex-shrink: 0 here, which just pushes the overflow into
   overlapping the next grid column instead of preventing it) is what
   actually keeps three ~4rem-wide selects from outgrowing their
   col-md-4 parent at the md breakpoint -- they wrap onto a second line
   there instead. */
.schedule-time-select {
  min-width: 4rem;
}

/* "Publish later" widget (ScheduledPublishWidget -- the Date input, its
   calendar-icon addon, and the three Time selects) -- transparent fills +
   the site-wide field border (static/shared/css/fields.css, like every
   other field on this page), so they sit on the page like the
   rest of the form rather than as white boxes. border-radius explicit at
   0.5rem (reported directly: unify every field's corner radius on this
   page to 8px) rather than Bootstrap's own default -- these three
   controls no longer need a separate var(--bs-border-radius) fallback to
   stay in step with Category/Subcategory/Subtitle/Tags above, since all
   of them now share this one literal value instead. On focus the border
   turns accent-blue and Bootstrap's own glow is dropped, matching
   .article-form-select's focus treatment. */
.scheduled-publish-widget .form-control,
.scheduled-publish-widget .form-select,
.scheduled-publish-widget .input-group-text {
  background-color: transparent;
  border-color: var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
}

.scheduled-publish-widget .form-control:focus,
.scheduled-publish-widget .form-select:focus {
  background-color: transparent;
  border-color: var(--site-link-color, #0067b8);
  box-shadow: none;
}

/* --- The Add / Edit article page: fields beside a Publishing card ---------- */

/* The same layout as videos.css's .video-form (the Add / Edit video form), byte
   for byte in its declarations: the learning path form would be the third
   user, and these move to static/shared/css/ with it. Two columns from 992px
   rather than the video form's 768px, because the body's toolbar needs the
   width. */

.article-form__breadcrumb {
  margin-bottom: 0;
}

/* Left-aligned, as on the builders and the video form (the standard heading
   is centred, base.html). */
.article-form > .page-heading {
  margin-top: 0.75rem;
  margin-bottom: 0.75rem;
  text-align: left;
}

.article-form > .page-heading .page-heading__subtitle {
  margin-top: 0.5rem;
  margin-inline: 0;
}

.article-form__problem .bi {
  margin-right: 0.25rem;
}

.article-form__layout {
  display: grid;
  gap: 0 1.5rem;
}

@media (min-width: 992px) {
  .article-form__layout {
    grid-template-columns: minmax(0, 1fr) 22rem;
    align-items: start;
  }
}

/* Beside the fields (from 992px) the card starts level with the first row
   under the Expand / Collapse all line, not with that line. */
@media (min-width: 992px) {
  .article-form__aside {
    margin-top: calc(var(--article-form-toggle-height) + 0.25rem);
  }
}

/* Pinned under the navbar only while the whole card fits the window: a
   sticky card taller than the window would keep its buttons out of view
   (with a cover image the checklist has ten rows). */
@media (min-width: 992px) and (min-height: 56rem) {
  .article-form__aside {
    position: sticky;
    top: calc(var(--lc-sticky-top, 4rem) + 1rem);
  }
}

/* The left column is a stack of accordions (native <details>, like the
   Publishing card's "Publish later"): a bordered row with an icon, the
   section's heading and a chevron that turns when it opens. Black and white,
   no fills; the border is the field colour (fields.css), as every other line
   on this page. Details and Article start open (they hold the required
   fields), Tags and Cover image open on their own when they have a value or
   an error. No overflow rule on them: the body's pinned toolbar needs them
   to leave sticky alone. */
.article-form__import,
.article-form__section {
  margin-bottom: 1rem;
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
}

/* A fixed height, so the aside (the Publishing card) can start level with the
   first row under it from 992px (--article-form-toggle-height, below). */
.article-form {
  --article-form-toggle-height: 2rem;
}

.article-form__toggle-all {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: var(--article-form-toggle-height);
  margin-bottom: 0.25rem;
}

/* Byte for byte the same as documents.css's .document-fill__collapse-all-btn
   (the template fill page's "Collapse all sections"). */
.article-form__collapse-all-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.875rem;
  font-weight: 600;
  text-decoration: none;
}

.article-form__collapse-all-btn:hover,
.article-form__collapse-all-btn:focus-visible {
  text-decoration: underline;
}

.article-form__summary {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.9rem 1.15rem;
  cursor: pointer;
  list-style: none;
}

.article-form__summary::-webkit-details-marker {
  display: none;
}

.article-form__summary-icon {
  font-size: 1.25rem;
}

.article-form__section-title {
  flex: 1;
  margin: 0;
  font-size: 1.15rem;
  font-weight: 700;
  color: #000;
}

.article-form__chevron {
  transition: transform 0.15s ease;
}

[open] > .article-form__summary .article-form__chevron {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .article-form__chevron {
    transition: none;
  }
}

.article-form__section-body,
.article-form__import-body {
  padding: 0.25rem 1.15rem 1.15rem;
}

.article-form__section-body > :last-child {
  margin-bottom: 0;
}

.article-form__publishing,
.article-form__publishing .card-header {
  border-color: var(--site-input-border-color, #767676);
}

/* The Draft / Published / Pending token in the Publishing card's subtitle,
   small enough that the header is a single line tall. */
.article-form__publishing .card__token {
  padding: 0 0.4rem;
  font-size: 0.8125rem;
  line-height: 1.25rem;
  vertical-align: middle;
}

/* The builder shell's tighter card strip (assessments.css). */
.article-form .builder-form-card .card-header.builder-form-card__header {
  padding: 0.85rem 1.35rem;
  background-color: transparent;
  border-bottom-color: var(--site-input-border-color, #767676);
}

.article-form .builder-form-card__title {
  font-size: 1.15rem;
}

.article-form .builder-form-card__subtitle {
  font-size: 0.9375rem;
}

.article-form__checklist {
  margin-bottom: 1.25rem;
}

.article-form__check {
  margin-bottom: 0.35rem;
}

/* The checklist's amber "worth adding" mark: Bootstrap's warning yellow is
   1.6:1 on white, too faint to see; #946200 is 5.2:1. !important beats
   the .text-warning utility (itself !important). */
.article-form__checklist .bi.text-warning {
  color: #946200 !important;
}

.article-form__check-hint {
  color: #5c6672;
}

.article-form__buttons {
  display: grid;
  gap: 0.6rem;
}

.article-form__delete {
  margin-top: 1rem;
}

/* "Publish later": the date and time stay folded away until asked for, so the
   card is not taller than most windows. It opens by itself after a refusal
   (the template adds `open`). */
.article-form__schedule {
  margin-top: 1rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--site-input-border-color, #767676);
}

.article-form__schedule > summary {
  font-weight: 600;
  cursor: pointer;
}

.article-form__schedule-body {
  padding-top: 0.75rem;
}

/* The body's Quill toolbar stays where Quill puts it, above the editor, and
   pins under the navbar while the editor is on screen. From 576px only: below
   it the toolbar wraps onto up to three rows, which would cover too much of a
   phone's window. Its parent (.document-richtext) is as tall as the editor, so
   it lets go when the editor ends. A box of its own, detached from the
   editor below by a gap (the border and radius of every other field here, an
   opaque fill so text scrolling under it doesn't show). This replaced
   article_body_toolbar.js, which moved the toolbar into the old Write /
   Publish tab bar (2026-11-05). */
.article-form .document-richtext .ql-toolbar.ql-snow {
  margin-bottom: 0.5rem;
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
  background-color: var(--site-bg-color, #fff);
}

@media (min-width: 576px) {
  .article-form .document-richtext .ql-toolbar.ql-snow {
    position: sticky;
    top: calc(var(--lc-sticky-top, 4rem) + 0.75rem);
    z-index: 5;
    /* The page colour over the 0.75rem between the navbar and the toolbar,
       so text scrolling past doesn't show in it. */
    box-shadow: 0 -0.75rem 0 var(--site-bg-color, #fff);
  }

  /* Pinned under the navbar (article_form.js adds .is-stuck): square bottom
     corners, so it reads as a bar rather than a floating box. */
  .article-form .document-richtext .ql-toolbar.ql-snow.is-stuck {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }
}

/* The body field's editable area, matching .article-detail__body's own
   real typography (below) rather than Quill's own default content font
   (quill.snow.css) -- the Write tab previews as the real published
   article, so what you're typing should read at the same size/
   line-height it'll actually publish at. Transparent background so the
   editable area sits flush against the page's own warm-cream background
   rather than reading as a separate white panel, same reasoning as the
   "remove white background" pass elsewhere on this form (see
   authored.html's own accordion-turned-tabs). Border/radius match this
   form's other boxed fields (the site-wide field border/0.5rem) on all
   four corners,
   unconditionally -- a complete, self-contained box regardless of
   whether the toolbar is currently showing up in the tabs bar above.
   The full `border` shorthand (not just border-color) used to matter
   here specifically because quill.snow.css's own `.ql-toolbar.ql-snow +
   .ql-container.ql-snow { border-top: 0 }` adjacency rule fired
   regardless of the toolbar's visibility as long as it stayed a true DOM
   sibling of this element -- moot now that the toolbar is relocated
   somewhere else entirely (never adjacent to .ql-container at all), but
   left explicit anyway as cheap insurance against that rule ever
   mattering again. */
#articleForm .document-richtext .ql-container.ql-snow {
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
  font-size: 1.0625rem;
}

/* The cover-image dropzone's own radius (static/shared/css/cards.css,
   0.75rem) is a site-wide default shared with every other cover-image
   field on the site -- overridden here just for this page's own
   instance (#articleFormImageDropzone, its own id, not the shared
   .cover-image-dropzone class) to the same literal 0.5rem/8px every
   other field on this page now uses (reported directly), without
   touching that shared default anywhere else it's used. overflow:
   hidden on the dropzone itself (cards.css) clips the preview image and
   every other child to this same rounded shape automatically, no
   separate override needed for them.

   #learningPathFormImageDropzone (learning_path_author_form.html's own
   cover-image dropzone -- same shared .cover-image-dropzone/ImageDropzoneWidget
   shape, different page, different id) gets the identical override for
   the same reason: it was still left at the shared 0.75rem/12px default,
   the one field out of step with the rest of that page's own 8px fields
   (reported directly: unify every field's corner radius on that page
   too). */
#articleFormImageDropzone,
#learningPathFormImageDropzone {
  border-radius: 0.5rem;
}

/* The last plain-Bootstrap-default fields on this page: the Cover image
   modal's three input-groups (alt text/credit name/credit URL) and the
   "Upload article from file" input. Setting --bs-border-radius (not a
   literal border-radius) on each .input-group lets Bootstrap's own
   internal corner-joining math do the right thing -- the icon box keeps
   square inner corners against the text input beside it, only the
   outer corners round, exactly like every other input-group on the
   site; a flat border-radius here would round all four corners of each
   piece and break that join. */
#articleForm .input-group {
  --bs-border-radius: 0.5rem;
}

#articleDocumentImportFile {
  border-radius: 0.5rem;
}

#articleForm .ql-editor {
  line-height: 1.7;
  background-color: transparent;
  /* A short minimum, so an empty/near-empty body doesn't reserve a
     tall block of mostly-blank space above the fold. */
  min-height: 150px;
  padding: 1.5rem;
}

#articleForm .ql-editor :is(h2, h3, h4) {
  margin-top: 2rem;
}

/* Full 2px accent ring around the whole box on focus, matching every
   other field on this form -- :focus-within (not a JS-toggled class)
   since .ql-editor, the real
   contenteditable, is a genuine descendant of .ql-container and focus
   works natively here. */
#articleForm .ql-container.ql-snow:focus-within {
  border-color: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8);
}

/* The "Write something brilliant..." placeholder -- quill.snow.css's own
   .ql-editor.ql-blank::before hardcodes left/right: 15px (matching its
   own default 12px/15px padding) and an italic style; both overridden to
   match .ql-editor's own bumped-up padding above (otherwise the
   placeholder text sits ~9px left of where typed text will actually
   start) and this page's plain, non-italic, muted-gray convention. */
#articleForm .ql-editor.ql-blank::before {
  left: 1.5rem;
  right: 1.5rem;
  color: #6c757d;
  font-style: normal;
}

/* article_author_form.html's Write tab -- Title kept its large,
   document-style heading type, but (like Category/Subcategory/Tags
   below) now sits in the same persistent-box treatment as this form's
   other fields instead of staying bare until focus: a boundary that
   only appears once you're already interacting with the field isn't a
   boundary a low-vision or first-time reader can rely on to find it.
   Border/radius/fill match .article-form-select's own box. Also reused
   by learning_path_author_form.html for its "Learning path name" field
   -- same heading-style box. */
.article-form-title-input {
  border: 1px solid var(--site-input-border-color, #767676);
  /* Literal 0.5rem/8px, matching every other field's radius on this page
     (reported directly: unify to 8px) -- not the branding-driven
     var(--site-card-border-radius) other cards on the site use. */
  border-radius: 0.5rem;
  /* Transparent, not a filled box -- same "sits on the page, not as a
     white box" treatment as .scheduled-publish-widget's own fields. */
  background: transparent;
  padding: 0.6rem 1rem;
  /* Was 2rem (reported directly: too large/heavy next to the rest of
     this form's own type scale). */
  font-size: 1.5rem;
  font-weight: 700;
  box-shadow: none;
}

/* Full 2px accent ring around the whole box, same as
   .article-form-select:focus -- not just a thicker bottom rule.
   !important on these :focus rings (title, subtitle, outcome, tags) beats
   Bootstrap's validation focus shadows (.form-control.is-invalid:focus and
   .was-validated, higher specificity), so a field with an error keeps the
   site's ring. */
.article-form-title-input:focus {
  border-color: transparent;
  background: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8) !important;
}

/* Same boxed treatment as Title just above, colored to match
   _article_detail_content.html's own real rendering of this field
   (a plain <p class="lead text-muted">) instead of Bootstrap's .lead
   class directly -- this is an <input>, not a <p>, and still needs the
   focus/box-shadow resets Title's own rule has. Radius matches every
   other field on this page (0.5rem/8px, same literal value as Title
   just above) -- the height is what still distinguishes this row of
   fields from Title's own taller box
   (line-height pinned to an absolute 1.5rem, padding at Bootstrap's own
   .form-control vertical default of 0.375rem, the same two numbers
   .article-form-tags-row hand-copies below) and the font-size: 1rem,
   same as Category's own default .form-select text (this field, and its
   own "Article subtitle" placeholder, used to run larger at 1.25rem for
   a "lead"-text feel, but that read as visibly out of step with every
   other field's own type size on this same row of fields) -- all four
   of Subtitle/Category/Subcategory/Tags render at one shared height and
   text size this way, matching the Publish tab's own time selects too. */
.article-form-subtitle-input {
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
  background: transparent;
  padding: 0.375rem 1rem;
  line-height: 1.5rem;
  font-size: 1rem;
  font-weight: 300;
  color: var(--bs-secondary-color, #6c757d);
  box-shadow: none;
}

.article-form-subtitle-input:focus {
  border-color: transparent;
  background: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8) !important;
}

/* learning_path_author_form.html's learning_outcome textarea -- a
   deliberate visual clone of the article body's Quill writing surface
   (.ql-editor above), minus the editor itself: transparent, borderless
   at rest, a generous inset padding, a comfortable reading size, and --
   on focus -- the same full 2px accent ring (.ql-container.ql-snow:focus-within)
   rather than a plain bottom rule. article_author_form.html has no direct
   analogue: its body IS a real Quill instance. */
.article-form-outcome-input {
  /* A hairline at rest so the empty field still reads as an input; on
     focus it gives way to the 2px accent ring below (border kept, just
     turned transparent, so the box never changes size). #767676 matches
     the bottom rule on this form's own selects / tags input (contrast
     rationale in .article-form-select above). */
  border: 1px solid #767676;
  background: transparent;
  padding: 1.5rem;
  font-size: 1.0625rem;
  line-height: 1.7;
  min-height: 150px;
  /* Literal 0.5rem/8px, matching every other field's radius on this page
     (reported directly: unify to 8px) -- used to be the theme's card
     radius (var(--site-card-border-radius), 1.25rem/20px by default),
     the one field left out of step with the rest once they were unified. */
  border-radius: 0.5rem;
  box-shadow: none;
  resize: vertical;
}

.article-form-outcome-input:focus {
  border-color: transparent;
  background: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8) !important;
}

/* The "Words: N  Characters: N" readout under learning_outcome
   (shared/js/word_char_counter.js) -- a touch smaller than body text, in
   the default text colour rather than the muted .form-text grey. */
.article-form-outcome-count {
  font-size: 0.875em;
}

/* Category + Subcategory (article_author_form.html) now lay out via
   plain Bootstrap .row.g-2 / .col-12.col-sm -- the same pattern as
   learning_paths' own Category/Subcategory filters
   (learning_paths/widgets/learning_path_articles.html, reported
   directly: match that page's viewport behaviour here): full-width
   stacked on an xs viewport, equal-width side by side at sm and up. No
   custom rule needed any more -- this replaced the former fixed-width
   flex-wrap row. */

/* Category/subcategory selects -- a full box (border on all four sides,
   transparent fill), not just a bottom rule: a persistent bottom-only
   underline reads as an input boundary only once you already know to
   look for it, and disappears from a quick visual scan or for anyone
   with low vision/cognitive load. The colour is the site-wide field
   border, --site-input-border-color (static/shared/css/fields.css;
   #767676 by default: 4.08:1 against a warm-cream page background /
   4.54:1 against plain white, clearing WCAG 1.4.11's 3:1 non-text
   minimum), shared with every other boxed field on this page
   (.ql-container, .article-form-title-input/-subtitle-input/-tags-input/
   -tags-row). It was #9aa5b1 for a while, to match .cover-image-
   dropzone's dashed border, until the 2026-10-16 accessibility audit
   (AX-07) flagged that at about 2.5:1. Padding left at Bootstrap's
   own default .form-select padding (no custom padding here) rather than
   the bumped-up box the rest of this form's fields use -- this select
   needs to render at the exact same height as the Publish tab's own
   "Publish later" time selects (#id_scheduled_at_hour/_minute/_ampm,
   .schedule-time-select, plain undecorated .form-selects further down
   this file), so Tags (below) and Subtitle (.article-form-subtitle-input
   above) are hand-matched to this same box instead. border-radius is the
   one literal 0.5rem every field on this page now shares (reported
   directly), not Bootstrap's own default any more -- .scheduled-publish-
   widget's own rule sets the matching value explicitly for the time
   selects too, so the whole family stays in step. The native dropdown
   chevron (.form-select's own background-image, untouched by only
   overriding background-color/border here) stays, so it's still visibly a
   dropdown, not a text field. */
.article-form-select {
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
}

.article-form-select:not(:disabled) {
  background-color: transparent;
}

/* A full 2px accent ring around the whole box on focus (same pattern as
   .ql-container.ql-snow:focus-within above), not just a thicker bottom rule -- a
   partial indicator on one edge of the control is too easy to miss
   entirely (WCAG 2.4.7 wants the *entire* control's focus state to be
   obviously visible, not just implied by one edge changing). border
   turns transparent instead of disappearing so the box never resizes at
   focus. box-shadow: none only drops Bootstrap's own default glow, not
   the browser's own native focus ring some engines (e.g. Firefox) still
   draw on a <select> regardless, which stays as an extra, unforced layer
   of visibility. */
.article-form-select:focus {
  border-color: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8);
}

/* Tags -- the plain, real submitted field (article_tags_editor.js's
   no-JS fallback: this stays visible, exactly as it always rendered,
   placeholder and all). Same full-box treatment as .article-form-select
   just above -- not fully bare like Title, since typed text here would
   otherwise read as plain words, not the tags they'll become. With JS,
   article_tags_editor.js hides this and the chip editor below takes
   over. Padding/radius left at Bootstrap's own .form-control default,
   same reasoning as .article-form-select above -- .form-control and
   .form-select share the same vertical padding (0.375rem) and
   line-height (1.5), so this renders at that same reference height. */
.article-form-tags-input {
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
  background-color: transparent;
  box-shadow: none;
}

.article-form-tags-input:focus {
  border-color: transparent;
  background-color: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8) !important;
}

/* Outer row article_author_form.html wraps around the leading bi-hash
   icon + article_tags_editor.js's own editor container -- same box
   treatment as .article-form-tags-input above (the field it replaces),
   just moved up here so the icon sits inside the same bordered row
   instead of floating above/below it. padding/line-height are a manual
   copy of Bootstrap's own .form-control values (0.375rem vertical /
   1.5rem line box) -- this row is a plain <div>, not a real
   .form-control, so it has no Bootstrap default of its own to inherit;
   matching those two numbers by hand is what makes it come out at the
   same height as Category/Subcategory/the Tags no-JS fallback (and the
   Publish tab's own time selects) despite the taller bi-hash icon
   sitting inside it. :focus-within so the whole row lights up while the
   typed-word input inside (see .article-form-tags-editor__input below)
   has focus, not just if the row itself somehow did. */
.article-form-tags-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid var(--site-input-border-color, #767676);
  border-radius: 0.5rem;
  background-color: transparent;
  padding: 0.375rem 0.75rem;
  line-height: 1.5rem;
}

.article-form-tags-row:focus-within {
  border-color: transparent;
  box-shadow: 0 0 0 2px var(--site-link-color, #0067b8);
}

/* Persistent help text under Category/Subcategory/Tags (article_author_
   form.html) -- darker than Bootstrap's own .form-text gray
   (--bs-secondary-color, #6c757d, a ~4.7:1 AA pass only against a plain
   white background) so it still clears WCAG's 4.5:1 normal-text minimum
   against this page's own warm-cream PageBackground option too (#6c757d
   drops to ~4.2:1 there and fails; #5c6672 holds ~5.2:1). Same gray
   already used for .assessment-page-head__lede, for the same reason. */
.article-form-hint {
  color: #5c6672;
}

/* A vertical rule right after the icon -- the same icon-cell/input seam
   an .input-group's .input-group-text produces (see the practice-
   templates/learning-path-articles search boxes above), hand-copied here
   since this row is a plain flex <div>, not a real .input-group.
   align-self: stretch fills the row's content-box height, but that box
   is already inset from the row's own border by .article-form-tags-row's
   0.375rem top/bottom padding -- the negative margin here cancels that
   inset back out so the rule reaches the row's actual top/bottom border,
   same full-height seam as a real .input-group-text's border. */
.article-form-tags-row__icon {
  flex: 0 0 auto;
  align-self: stretch;
  margin: -0.375rem 0;
  display: flex;
  align-items: center;
  padding-right: 0.75rem;
  border-right: 1px solid var(--site-input-border-color, #767676);
  color: var(--bs-secondary-color, #6c757d);
}

/* article_tags_editor.js's typed-word input, the only thing left inside
   the bordered row (existing tags render as pills in
   .article-form-tags-pills, below the row -- see that rule). Grows to
   fill whatever width the row's icon leaves it (flex: 1 1 auto +
   min-width: 0 so it can still shrink instead of overflowing the row). */
.article-form-tags-editor {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  align-items: center;
}

.article-form-tags-editor__input {
  flex: 1 1 8rem;
  min-width: 6rem;
  border: none;
  background: transparent;
  padding: 0.125rem 0;
  /* 1rem, matching Category/Subcategory's own default .form-select text
     size (and Subtitle's own, just above) -- this is the typed-word
     input an author actually sees/types into once JS is available (the
     no-JS-fallback #id_tags input it replaces is a plain .form-control,
     already 1rem by Bootstrap's own default with no override needed). */
  font-size: 1rem;
  /* !important: beats focus.css's .form-control:focus halo, a more
     specific rule; the editor box draws the ring (:focus-within). */
  box-shadow: none !important;
  outline: none;
}

/* This input carries no .form-control class (a plain element built by
   article_tags_editor.js, not rendered by Django), so it falls back to
   the browser's own lighter default placeholder gray instead of
   Bootstrap's .form-control::placeholder rule -- same darker color as
   Title/Subtitle's own placeholders (.article-form-title-input/
   .article-form-subtitle-input, both plain Bootstrap .form-control
   elements) for a readable, consistent hint across the whole tab. */
.article-form-tags-editor__input::placeholder {
  color: var(--bs-secondary-color, #6c757d);
}

/* Existing tags render here, under the search/typed-word row above (not
   inside it) -- a wrapping row of pills, spaced the same as the row's own
   icon-to-input gap. Empty (no width/height of its own) until
   article_tags_editor.js populates it, so it's harmless to leave
   unhidden before JS runs. */
.article-form-tags-pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

/* A pill itself never grows/shrinks with the row (its own text stays a
   fixed, readable width). Larger + bold + the same muted gray as
   Title/Subtitle's own hint/placeholder treatment above -- form-only
   (scoped to .article-form-tags-pills, not the bare .article-detail__tag-
   badge class itself), so the real published page's own tag pills
   (_article_detail_content.html) are untouched. */
.article-form-tags-pills .article-detail__tag-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  flex: 0 0 auto;
  cursor: default;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--bs-secondary-color, #6c757d);
}

/* Plain "×", not a bi- icon -- small enough at this pill's own font-size
   (0.8125rem) that an icon glyph would just look like a stray smudge;
   a bare character reads cleanly at this scale. Muted at rest (a pill's
   own border color, #6c757d) so a row of tags doesn't read as a row of
   warnings; darkens to the pill's own :hover/:focus-visible text color
   on interaction. */
.article-form-tags-editor__remove {
  border: none;
  background: none;
  padding: 0;
  margin: 0;
  line-height: 1;
  font-size: 1rem;
  color: #6c757d;
}

.article-form-tags-editor__remove:hover,
.article-form-tags-editor__remove:focus-visible {
  color: #212529;
}

/* .form-select:disabled's own background-color (Bootstrap's --bs-
   secondary-bg tint) is deliberately left alone by the :not(:disabled)
   rule above -- it's the one built-in visual cue a disabled select
   already has, and removing it would make "disabled" and "enabled but
   empty" indistinguishable. */

/* "Edit cover image" / "Remove cover image" under the cover dropzone
   (article_author_form.html, learning_paths/author_form.html): the
   assessment builder Overview step's link-styled pair, at the builder's
   1rem text-size floor for small controls (`.assessment-builder .btn-sm`,
   assessments.css) rather than Bootstrap's 0.875rem, so the two look the
   same (product owner, 2026-10-19). Generic name: any cover-image row
   could use it. */

/* .cover-image-dropzone / .cover-image-preview / .image-focal-editor__*
   (the create-with-a-photo control's generic shape) now live in
   static/shared/css/cards.css -- documents becoming a 3rd byte-for-byte
   consumer was the "extract on the 3rd" trigger
   (agent_docs/frontend-css-conventions.md). Only this form's own
   per-instance override stays here: article_author_form.html's
   featured_image and learning_path_author_form.html's cover_image both
   author at 16:9 (.article-detail__hero's own 1920/1080), overriding
   the shared block's 3/2 default. */
#articleFormFocalEditor .image-focal-editor__frame,
#learningPathFormFocalEditor .image-focal-editor__frame {
  aspect-ratio: 1920 / 1080;
}

.learning-path-card__outcome {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Muted "5 articles · ~24 min" meta line under a card title. Generic
   visual pattern (a small muted line of facts), so it gets a generic
   name with no app prefix per agent_docs/frontend-css-conventions.md --
   define it once per app stylesheet that uses it until a third app needs
   a byte-for-byte copy, then it graduates to static/shared/css/cards.css.
   Articles itself doesn't render this line (its own read-time badge is
   .article-card__read-time below) -- learning_paths loads this same
   stylesheet and is the only current user. font-size matches
   .article-card__read-time's Bootstrap .small (.875em, reported directly
   as a mismatch between the two cards' estimated-time text). color is
   the same var(--bs-secondary-color, #6c757d) that underlies
   .article-card__read-time's own .text-muted (Bootstrap 5.3's
   .text-muted resolves to that token, not a flat #6c757d -- reported
   directly as a colour mismatch between the two cards' article-count/
   read-time text). */
.card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  color: var(--bs-secondary-color, #6c757d);
}

.card__meta .card__meta-sep {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background-color: currentColor;
  opacity: 0.55;
}

.card__meta .card__meta-total {
  color: #0067b8;
  font-weight: 600;
}

/* Learning-path cards: the reader card (learning_paths/_reader_card.html,
   [data-lp-card] -- the Learning paths catalogue and My library's Enrolled
   learning paths) and the management card (_card.html, My learning paths).
   Both GitHub-style since 2026-10-18 (product owner), the same as
   .article-card: no border, fill, shadow or padding, 0.5rem corners on card
   and cover, a labels row under the cover (Enrolled / Completed, or the
   publishing status), the title in the heading colour, the outcome as a
   muted teaser, the "5 articles · ~24 min" line in the article card's
   read-time spot, then text links in the footer. No fixed heights:
   .card__spacer lines the footers up across a row. */
.learning-path-card {
  --bs-card-border-width: 0;
  --bs-card-bg: transparent;
  display: flex;
  flex-direction: column;
  border-radius: 0.5rem;
}

.learning-path-card .card-body {
  padding: 0;
}

.learning-path-card .learning-path-card__image-wrap {
  aspect-ratio: 16 / 9;
}

.learning-path-card .learning-path-card__image {
  aspect-ratio: 16 / 9;
  height: 100%;
}

/* The reader card writes its row on one line, so it's :empty (and takes
   no space) when neither Enrolled nor Completed applies. */
.learning-path-card__labels:empty {
  display: none;
}

.learning-path-card__title a {
  color: var(--site-heading-color, #1d1d1f);
}

/* Left out by both templates when the path has no outcome. truncatewords
   cuts at a word boundary; the 4-line clamp is the safety net. */
.learning-path-card__summary {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-height: 6em;
  margin-bottom: 0.75rem;
  color: #5c6672;
  font-size: 1rem;
  line-height: 1.5;
}

/* 1.5rem before the footer, as under the article card's read time
   (0.75rem before a library card's progress bar). */
.learning-path-card__meta {
  margin-bottom: 1.5rem;
}

.learning-path-card__meta--tight {
  margin-bottom: 0.75rem;
}

/* Slim progress bar on the card face -- distinct from the chunkier
   .learning-path-progress bar on the detail page / article page. */
.learning-path-card__progress {
  height: 0.375rem;
  background-color: #dee2e6;
  border-radius: 980px;
  overflow: hidden;
}

.learning-path-card__progress-fill {
  height: 100%;
  background-color: #0067b8;
  border-radius: 980px;
}

.learning-path-card__progress-fill--done {
  background-color: #1a7f37;
}

.learning-path-card__progress-label {
  margin-top: 0.3rem;
  margin-bottom: 1.5rem;
  font-size: 0.75rem;
  color: #6c757d;
}

/* The card face is one big Preview trigger (the title's .stretched-link
   overlays it and opens the offcanvas). Each footer control is lifted
   above that overlay so it keeps its own click target. */
.learning-path-card__quicklook-btn,
.learning-path-card__enroll-btn {
  position: relative;
  z-index: 2;
}

/* _learning_path_quicklook.html offcanvas -- a touch wider than
   Bootstrap's 400px default so the numbered article list isn't cramped. */
@media (min-width: 576px) {
  .offcanvas.learning-path-quicklook {
    width: 27rem;
  }
}

/* overflow:hidden on the wrap (not the <img>) so the focal-point zoom
   (transform: scale below) is cropped to the box instead of spilling over
   the offcanvas -- same split as .article-card__image-wrap /
   .learning-path-card__image-wrap. 16 / 9 matches the browse card's own
   cover ratio (.learning-path-card .learning-path-card__image-wrap). */
.learning-path-quicklook__cover-wrap {
  overflow: hidden;
  aspect-ratio: 16 / 9;
}

/* Bumped up from Bootstrap's .small (0.875em) -- the learning outcome is
   the lead-in sentence for the tray, not a footnote. */
.learning-path-quicklook__outcome {
  font-size: 1rem;
}

.learning-path-quicklook__cover,
.learning-path-preview__cover {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal-x, 50%) var(--focal-y, 50%);
  transform: scale(var(--zoom, 1));
  transform-origin: var(--focal-x, 50%) var(--focal-y, 50%);
}

.learning-path-quicklook__list .list-group-item.is-read {
  color: #6c757d;
}

.learning-path-quicklook__list .list-group-item.is-read .lp-quicklook__num {
  color: #1a7f37;
}

/* "Read an article" / "Preview learning path" modal header
   (learning_paths/_article_modal.html) -- site logo at the far left,
   the loaded article's category/subcategory meta (moved up here by
   article_modal.js's setModalMeta()) grouped with the close button at
   the far right. */
.article-modal__header {
  gap: 1rem;
}

.article-modal__brand .navbar-brand-logo {
  height: 20px;
}

.article-modal__meta {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #6c757d;
  text-transform: uppercase;
  text-align: right;
}

/* "Preview learning path" modal (_learning_path_preview.html) -- a
   full-bleed cover across the top of the modal body, same focal-point
   crop treatment (--focal-x/--focal-y/--zoom, inline) as the card/row
   images above. Also reused (unmodified) by learning_path_detail.html's
   own cover. overflow:hidden on the wrap (not the <img>) so the zoom is
   cropped to the box rather than spilling over the neighbouring column /
   modal edge -- same split as .article-card__image-wrap. */
.learning-path-preview__cover-wrap {
  overflow: hidden;
  aspect-ratio: 3 / 2;
  border-radius: 0.5rem;
  margin-bottom: 1rem;
}

/* Publish-tab readiness checklist item link (article_author_form.html +
   static/shared/js/publish_checklist.js) -- jumps focus to the real field
   back on the Write tab (or its cover-image modal). Generic name/no app
   prefix since it's part of the shared publish_checklist.js widget, kept
   here rather than in static/shared/css/cards.css until a 2nd app uses
   it (same one-app-so-far threshold as every other shared-pattern rule --
   see agent_docs/frontend-css-conventions.md). Plain inherited color +
   no underline at rest -- the item <li> itself, not this link, already
   carries the done/muted/optional color via .text-success/.text-muted
   (JS-toggled) -- underline only on hover/focus is what marks it
   interactive. */
.publish-checklist__link {
  color: inherit;
  text-decoration: none;
}

.publish-checklist__link:hover,
.publish-checklist__link:focus-visible {
  text-decoration: underline;
}

/* --- End-of-article hand-off panels: "Put this into practice"
   (documents/_practice_templates.html, {% practice_templates %}) and
   "Build on What You Learned" (_article_detail_content.html). One look
   for both since 2026-10-19 (product owner): black and white, no tinted
   background, a 1px black border, neutral icons, and bordered rows that
   turn black on hover. Each rule below names both panels' classes so the
   two can't drift apart. The .practice-panel* selectors move to
   static/shared/css/cards.css once a second host app renders the tag
   (see that template's own comment). The learning-paths rows keep their
   article-detail__ names, which the template carries for its group
   labels. --- */
.practice-panel,
.learning-paths-panel {
  position: relative;
  overflow: hidden;
  margin: 2.5rem 0;
  padding: 2rem 2.25rem;
  border-radius: 1.5rem;
  background: none;
  border: 1px solid #000;
}

.practice-panel__head,
.learning-paths-panel__head {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 1.35rem;
}

/* Neutral, like the card labels (.card__token's #f2f5f3): a black glyph
   on pale grey, no colour. */
.practice-panel__icon,
.learning-paths-panel__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: 0.85rem;
  background: #f2f5f3;
  color: #1d1d1f;
  font-size: 1.35rem;
}

.practice-panel__title,
.learning-paths-panel__title {
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--site-text-color, #16233c);
  margin: 0 0 0.35rem;
}

.practice-panel__lede,
.learning-paths-panel__lede {
  color: var(--site-muted-text-color, #5c6672);
  margin: 0;
  max-width: 40rem;
}

.article-detail__learning-path-group + .article-detail__learning-path-group {
  margin-top: 1.5rem;
}

/* Sentence case, as written ("Continue the series" / "Explore related
   learning"); it was all caps (product owner, 2026-10-19). */
.article-detail__learning-path-group-label {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--site-muted-text-color, #5c6672);
  margin: 0 0 0.65rem;
}

.practice-panel__list,
.article-detail__learning-path-list {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.practice-panel__item,
.article-detail__learning-path-item {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.85rem 1.1rem;
  background: var(--site-card-bg-color, #fff);
  border: 1px solid #dee2e6;
  border-radius: 0.85rem;
  text-decoration: none;
  transition: border-color 0.15s ease;
}

/* A quiet hover that's still easy to see (product owner, 2026-10-19; it
   was a lift with a shadow, then a pale grey fill too close to white to
   notice, about 1.06:1): the row's border turns black, echoing the
   panel's own black border, and the row's name is underlined. Nothing
   moves, and no fill, so the grey icon tile stays visible. The underline
   is the cue that doesn't rely on colour. */
.practice-panel__item:hover,
.practice-panel__item:focus-visible,
.article-detail__learning-path-item:hover,
.article-detail__learning-path-item:focus-visible {
  border-color: #000;
}

.practice-panel__item:hover .practice-panel__item-title,
.practice-panel__item:focus-visible .practice-panel__item-title,
.article-detail__learning-path-item:hover .article-detail__learning-path-name,
.article-detail__learning-path-item:focus-visible .article-detail__learning-path-name {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Neutral like the panel's own icon (2026-10-19): the same black glyph on
   pale grey for every row. The glyph tells an assessment from a template
   in the practice panel; the group labels ("Continue the series" /
   "Explore related learning") tell the learning-path rows apart. */
.practice-panel__item-icon,
.article-detail__learning-path-icon,
.article-detail__learning-path-icon--related {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 0.65rem;
  font-size: 1.1rem;
  background: #f2f5f3;
  color: #1d1d1f;
}

.practice-panel__item-body,
.article-detail__learning-path-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.practice-panel__item-title,
.article-detail__learning-path-name {
  font-weight: 700;
  color: var(--site-link-color, #1668c9);
}

.practice-panel__item-desc,
.article-detail__learning-path-desc {
  color: var(--site-muted-text-color, #5c6672);
  font-size: 0.9rem;
}

/* #6c757d, 4.7:1 on white (it was #9aa5b1, about 2.5:1, under WCAG
   1.4.11's 3:1); black with the border on hover / focus. */
.practice-panel__item-chevron,
.article-detail__learning-path-chevron {
  flex: 0 0 auto;
  color: #6c757d;
}

.practice-panel__item:hover .practice-panel__item-chevron,
.practice-panel__item:focus-visible .practice-panel__item-chevron,
.article-detail__learning-path-item:hover .article-detail__learning-path-chevron,
.article-detail__learning-path-item:focus-visible .article-detail__learning-path-chevron {
  color: #000;
}

/* My library's In progress / Completed switch (learning_paths/
   _library_section.html, inside the Learning Paths accordion): kept on one
   line, counts included, inside the accordion's padding on a 320px phone. */
@media (max-width: 399.98px) {
  .library-progress-switch .segmented-tabs__btn {
    padding-left: 0.375rem;
    padding-right: 0.375rem;
    font-size: 0.75rem;
  }
}
