/* Generic reusable card patterns shared by every app with a card grid
   (articles, assessments, documents, and any future one) -- extracted
   here once a third app needed a byte-for-byte copy of these rules, per
   agent_docs/frontend-css-conventions.md's own "extract once a third app
   needs it" trigger. Themed via the --site-card-* custom properties
   branding._site_card_vars.html renders (Site Theme > Cards & Components
   admin page) -- see that app's docs for the full explanation.

   App-specific pieces (per-content-type badge/divider *colors*, image
   aspect ratios, layout) stay in each app's own stylesheet -- only the
   shape/behavior shared by all of them lives here. */

.card--lift {
  position: relative;
  background-color: var(--site-card-bg-color, #fff);
  border-radius: var(--site-card-border-radius, 1.25rem);
  box-shadow: var(--site-card-shadow, none);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.card--lift:hover,
.card--lift:focus-within {
  transform: var(--site-card-lift-transform, translateY(-4px));
  box-shadow: var(--site-card-lift-shadow, 0 10px 24px rgba(0, 0, 0, 0.12));
}

/* The GitHub-style catalogue cards (articles, learning paths, assessments,
   courses, videos, templates) don't carry .card--lift. They rise on hover
   and focus like it does, from the same Site Theme variable (Cards &
   Components > hover lift), but with no shadow: they are borderless and
   unfilled, so a shadow would draw a box around them. */
.article-card,
.learning-path-card,
.assessment-card,
.course-card,
.video-card,
.template-card {
  transition: transform 0.15s ease;
}

.article-card:hover,
.article-card:focus-within,
.learning-path-card:hover,
.learning-path-card:focus-within,
.assessment-card:hover,
.assessment-card:focus-within,
.course-card:hover,
.course-card:focus-within,
.video-card:hover,
.video-card:focus-within,
.template-card:hover,
.template-card:focus-within {
  transform: var(--site-card-lift-transform, translateY(-4px));
}

@media (prefers-reduced-motion: reduce) {
  .article-card,
  .learning-path-card,
  .assessment-card,
  .course-card,
  .video-card,
  .template-card {
    transition: none;
  }

  .article-card:hover,
  .article-card:focus-within,
  .learning-path-card:hover,
  .learning-path-card:focus-within,
  .assessment-card:hover,
  .assessment-card:focus-within,
  .course-card:hover,
  .course-card:focus-within,
  .video-card:hover,
  .video-card:focus-within,
  .template-card:hover,
  .template-card:focus-within {
    transform: none;
  }
}

/* Cover images zoom in slightly while the pointer is over the card or focus
   is inside it (product owner, 2026-10-27), on every card with a photo: the
   GitHub-style cards above plus the shop's product card. The image's own
   --zoom (the crop editor's zoom, set inline) is kept and multiplied, so a
   cropped cover grows from its saved size around its saved focal point. The
   box that holds the image already has overflow: hidden and rounded corners,
   so the growth is clipped to the cover. The gradient fallback covers are
   left out (nothing to zoom; the template ones animate their own circles).
   Movement is skipped under prefers-reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
  .article-card__image:not(.article-card__image--fallback),
  .learning-path-card__image:not(.learning-path-card__image--fallback),
  .assessment-card__image:not(.assessment-card__image--fallback),
  .course-card__image:not(.course-card__image--fallback),
  .template-card__image:not(.template-card__image--fallback),
  .product-card__image:not(.product-card__image--fallback) {
    transition: transform 0.4s ease;
  }

  .article-card:is(:hover, :focus-within) .article-card__image:not(.article-card__image--fallback),
  .learning-path-card:is(:hover, :focus-within) .learning-path-card__image:not(.learning-path-card__image--fallback),
  .assessment-card:is(:hover, :focus-within) .assessment-card__image:not(.assessment-card__image--fallback),
  .course-card:is(:hover, :focus-within) .course-card__image:not(.course-card__image--fallback),
  .template-card:is(:hover, :focus-within) .template-card__image:not(.template-card__image--fallback),
  .product-card:is(:hover, :focus-within) .product-card__image:not(.product-card__image--fallback) {
    transform: scale(calc(var(--zoom, 1) * 1.06));
  }
}

/* Opt-in light-gray outline on an otherwise-borderless card (.card--lift is
   shadow-based elevation only). Extracted here once a third component needed
   a byte-for-byte copy -- articles' .article-card, billing's .pricing-card,
   and assessments' .assessment-card -- per
   agent_docs/frontend-css-conventions.md's "extract on the 3rd" rule. */
.card--bordered {
  border: 1px solid #dee2e6;
}

.card__badge {
  display: inline-block;
  color: #fff;
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 0.3rem 0.75rem;
  border-radius: 980px;
}

.card__divider {
  margin: 0.75rem 0;
  border: none;
  border-top: 1px solid #adb5bd;
}

/* A neutral label, GitHub's "Token" (the "Whitepaper" label on its
   resource cards): 16px text, a pale grey fill, 4px corners. Used under the
   cover on the GitHub-style cards: articles, learning paths and the
   assessments catalogue (moved here from articles.css when assessments
   became its third app). Each app's colour modifiers (Pinned, Read,
   Draft / Published, ...) set only the fill and text, from its own sheet,
   which loads after this one. #1d1d1f on #f2f5f3 is about 15.3:1. */
.card__token {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.375rem 0.5rem;
  border-radius: 0.25rem;
  background-color: #f2f5f3;
  color: var(--site-text-color, #1d1d1f);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
}

/* "Create new" placeholder tile for a card grid -- a dashed-outline call
   to action sized to sit inline as the first cell of a .row card grid, or
   as the lone cell in an empty state. height:100% makes it match the
   row's tallest real card; min-height keeps it substantial when it stands
   alone. Extracted here once a third app (documents) needed a byte-for-byte
   copy -- articles' _add_article_card.html / _add_learning_path_card.html,
   assessments' _add_assessment_card.html, documents' _add_template_card.html
   -- per agent_docs/frontend-css-conventions.md's "extract on the 3rd" rule. */
.add-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.625rem;
  height: 100%;
  min-height: 12rem;
  padding: 2rem 1rem;
  border: 2px dashed #9aa5b1;
  border-radius: var(--site-card-border-radius, 1.25rem);
  color: #343a40;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  background-color: transparent;
  transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}

.add-card:hover,
.add-card:focus-visible {
  border-color: var(--site-link-color, #0067b8);
  color: var(--site-link-color, #0067b8);
  background-color: rgba(0, 103, 184, 0.05);
}

/* Soft brand-blue disc around the "+" -- enough of an accent that the
   tile isn't overlooked, without shouting; it fills in on hover. */
.add-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  background-color: rgba(0, 103, 184, 0.1);
  color: var(--site-link-color, #0067b8);
  font-size: 1.35rem;
  line-height: 1;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.add-card:hover .add-card__icon,
.add-card:focus-visible .add-card__icon {
  background-color: var(--site-link-color, #0067b8);
  color: #fff;
}

/* Cover-image dropzone + crop/zoom editor -- the create-with-a-photo
   control used by articles' / learning paths' author forms, the
   assessment builder's Overview step, and documents' create-template
   form. Extracted here once documents became the 3rd byte-for-byte
   consumer (agent_docs/frontend-css-conventions.md's "extract on the
   3rd" rule -- was duplicated in articles.css and assessments.css,
   each still carrying only their own per-instance overrides, e.g.
   #assessmentFormImageDropzone's --cover-aspect-ratio).

   Wraps ImageDropzoneWidget's own markup + .cover-image-preview as two
   stacked layers filling the same box, rather than an empty upload
   prompt sitting above a second, separate preview image -- each app's
   own *_image_focal_editor.js sets the has-image class to fade the
   dropzone's own prompt chrome out once there's an image to show
   underneath it. aspect-ratio reads --cover-aspect-ratio (a caller may
   set it inline to match that field's own real rendered ratio); the
   default, 1920/1080 (16:9), covers articles'/learning paths' own
   featured_image/cover_image. */
.cover-image-dropzone {
  position: relative;
  aspect-ratio: var(--cover-aspect-ratio, 1920 / 1080);
  border: 2px dashed #9aa5b1;
  border-radius: 0.75rem;
  overflow: hidden;
  transition: border-color 0.15s ease;
}

.cover-image-dropzone:hover,
.cover-image-dropzone:focus-within {
  border-color: var(--site-link-color, #0067b8);
}

/* The ghost-tile border is an empty-state affordance ("drop a file
   here") -- once has-image is set, the box is a photo, not a target,
   so the dashed outline comes off (:hover/:focus-within above no
   longer apply either, having no border left to color). */
.cover-image-dropzone.has-image {
  border: none;
}

/* Ghost-tile look (matches .add-card's own dashed border + no fill) --
   ImageDropzoneWidget's shared #e9ecef gray fill (image_dropzone.css)
   is scoped out here rather than removed globally, since every other
   caller (HeroSlideAdmin etc.) keeps it. */
.cover-image-dropzone .image-dropzone-widget {
  background-color: transparent;
}

.cover-image-dropzone .image-dropzone-widget,
.cover-image-dropzone .cover-image-preview {
  position: absolute;
  inset: 0;
  margin: 0;
}

/* The dropzone's own label (and, once has-image, its "click to replace"
   button) has to paint over the preview image sitting behind it --
   without this it's the reverse, DOM order alone would put the image
   (the later sibling) on top and hide the button entirely. */
.cover-image-dropzone .image-dropzone-widget {
  z-index: 1;
}

.cover-image-dropzone .cover-image-preview {
  z-index: 0;
}

.cover-image-dropzone .image-dropzone-widget__zone {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

/* has-image: set server-side when editing something that already has
   one, or by the page's own focal-editor script the instant a new file
   is chosen. Fades the empty-state icon/text out (the gray fill is
   already gone in every state, above), leaving just the image with the
   upload button as a "click to replace" hint over a scrim. The button
   sits vertically centered, not pinned to the bottom, so the scrim
   can't just fade out toward the top the way a bottom-only gradient
   would -- a dark photo there left the hint unreadably close in tone to
   whatever the image happened to be behind it. A uniform dark tint
   across the whole box (never fading below 0.35 alpha, not just at the
   bottom) plus overriding the button to a light "ghost" style (white
   text/border, not dark) reads reliably over a photo of any brightness,
   not just a bright one. */
.cover-image-dropzone.has-image .image-dropzone-widget__icon,
.cover-image-dropzone.has-image .image-dropzone-widget__text {
  display: none;
}

.cover-image-dropzone.has-image .image-dropzone-widget__zone {
  background: linear-gradient(to top, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.35));
}

.cover-image-dropzone.has-image .image-dropzone-widget__button {
  margin-top: 0;
  color: #fff;
  border-color: rgba(255, 255, 255, 0.85);
}

.cover-image-dropzone.has-image .image-dropzone-widget__button:hover {
  color: #212529;
  background-color: #fff;
  border-color: #fff;
}

/* The persistent, non-interactive cover image preview itself -- each
   app's own focal-editor script keeps this in sync with the modal
   editor's own live crop, same --focal-x/--focal-y/--zoom custom
   properties as .image-focal-editor__frame img below, so what's shown
   here matches the real saved crop exactly, just without the drag
   cursor/marker since this copy isn't itself draggable. No background
   fill of its own -- its <img> covers the box entirely once has-image
   is set (the only time this is visible at all), and
   .cover-image-dropzone's own ghost-tile look should show through
   otherwise. */
.cover-image-preview img {
  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%);
}

/* Position/zoom crop editor paired with the dropzone above -- spans the
   full width of its form's content column (ImageDropzoneWidget's own
   "Current image" preview is turned off for this field --
   show_current_preview=False -- so this is the only preview, and a
   larger frame is a more precise drag target). 3/2 is the shared
   default; a caller may override it with its own #id selector to match
   its own real rendered ratio (e.g. articles'/learning paths' 16:9).
   touch-action: none stops the browser's own scroll/pan gesture from
   competing with a drag on a touchscreen. */
.image-focal-editor__frame {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border: 1px solid #dee2e6;
  border-radius: 0.5rem;
  cursor: grab;
  touch-action: none;
  background-color: #e9ecef;
}

.image-focal-editor__frame:active {
  cursor: grabbing;
}

/* --focal-x/--focal-y/--zoom here are the *live*, still-unsaved values
   the drag/slider are setting -- kept in sync with the very same three
   custom properties each model's own *_focal_style property renders
   inline wherever the saved image actually displays, so this preview
   matches the real crop exactly once saved. */
.image-focal-editor__frame img {
  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%);
  pointer-events: none;
  user-select: none;
}

.image-focal-editor__marker {
  position: absolute;
  width: 1.25rem;
  height: 1.25rem;
  left: var(--focal-x, 50%);
  top: var(--focal-y, 50%);
  transform: translate(-50%, -50%);
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}

/* Generic "← Back to <parent>" link above a leaf page's content
   (assessment_detail.html, document_detail.html, product_detail.html,
   etc.) -- promoted here from assessments.css's `.back-link` +
   documents.css's `.document-detail__back-link` on shop.css needing the
   same rule a 3rd time (code-audit finding FE-05, 2026-09-17; shop's own
   copy was an incomplete, visually-drifted `.product-detail__back-link`
   with no color/weight/hover styling at all). Every app now shares this
   one generic class instead of its own app-prefixed copy. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-bottom: 1rem;
  color: #0067b8;
  font-weight: 600;
  font-size: 0.875rem;
  text-decoration: none;
}

.back-link:hover,
.back-link:focus-visible {
  text-decoration: underline;
}

/* Resizes/rounds a stock Bootstrap .btn (any color variant -- btn-primary,
   btn-outline-secondary, btn-outline-danger, ...) to the same size/shape
   as articles.css's own .article-card__read-btn, without touching its
   color -- unlike that class (and documents' .document-card__pill-btn),
   which are full custom-styled controls with their own background/color
   baked in, this is a shape-only modifier meant to sit alongside a real
   .btn-* class. First used on article_author_form.html's own
   Save/Preview/Cancel/Delete row and "Edit image" button (reported:
   the stock square .btn read as out of place next to this site's rounded
   card buttons elsewhere). Promoted here from articles.css once
   assessments' builder Overview step and documents' builder Settings step
   both reached for the same shape (reported directly: match article's own
   "Edit image" button on every other page with the same cover-image
   feature) -- the "extract on the 3rd app" trigger this file's own header
   comment and agent_docs/frontend-css-conventions.md both document. */
.btn-pill {
  padding: 0.7rem 1.4rem;
  border-radius: 980px;
  font-size: 0.9375rem;
  font-weight: 700;
}

/* .btn-check is Bootstrap's own toggle/radio-button-group pattern (a
   visually-hidden checkbox + a sibling label styled as a .btn) --
   Bootstrap's own `.btn-check + .btn:hover` rule (specificity 0,3,0,
   beats the generic `.btn:hover` it ships right above it in
   bootstrap.min.css) resets hover straight back to the *rest* colors
   by design, so hovering one button in a toggle/radio group doesn't
   visually "unpress" a checked sibling. This project reaches for that
   same .btn-check + <label class="btn"> markup in three places purely
   as a one-shot "Remove image" action, not a toggle/radio group at all
   -- just a plain button that happens to submit via a hidden checkbox
   (Django's ClearableFileInput "<name>-clear" convention:
   articles/article_author_form.html, documents/builder/meta_step.html,
   learning_paths/author_form.html) -- so that reset left all three with
   literally no visible hover state (reported directly). Promoted
   straight here rather than duplicated per app first, since all three
   already needed it (frontend-css-conventions.md's "extract on the 3rd
   occurrence" threshold). .btn-check-action on the label restores
   normal hover for exactly that one-shot case, at the same 0,3,0
   specificity so source order (this file loads after bootstrap_css in
   base.html) decides it, without touching .btn-check's real toggle/
   radio-group behavior anywhere else on the site. */
.btn-check + .btn-check-action:hover {
  color: var(--bs-btn-hover-color);
  background-color: var(--bs-btn-hover-bg);
  border-color: var(--bs-btn-hover-border-color);
}

/* .card__meta -- the generic muted icon+text meta line (agent_docs/
   frontend-css-conventions.md). Its colour lives here now that three apps
   use the class (each app's stylesheet keeps its own size/layout, which
   differ on purpose). #5c6672 clears WCAG 1.4.3's 4.5:1 on white (5.9:1)
   and on the pale builder tints it sits on (#e9f2fc, #eef5fd, #f8f9fa);
   Bootstrap's #6c757d did not on the tints (about 4.1 to 4.4:1,
   accessibility audit 2026-10-16, AX-10). articles.css keeps its own
   var(--bs-secondary-color), which matches .text-muted on the article
   cards and also passes. */
.card__meta {
  color: #5c6672;
}

/* .builder-form-card -- the one form card of the assessment and template
   builders (agent_docs/assessment-builder-design.md): a pale-blue header
   strip over a plain body, no nested cards. Moved here from assessments.css
   and documents.css (byte-for-byte copies) when the project workspace
   (projects/project_detail.html) became its third user, 2026-10-20. Each
   app's stylesheet keeps its own sizes for its pages (a tighter strip and a
   smaller title where several cards run down a page). overflow: hidden
   clips the square-cornered header strip (and, when a collapsible card is
   folded so the header is the whole card, its square bottom corners) to
   the card's own border radius. #1668c9 on #e9f2fc 4.8:1, #495862 on
   #e9f2fc 6.5:1. */
.builder-form-card {
  overflow: hidden;
}

.builder-form-card .card-header.builder-form-card__header {
  background-color: #e9f2fc;
  border-bottom: 1px solid #cfe1f2;
  padding: 1.25rem 1.35rem;
}

/* No dead space under a title that has no subtitle beneath it. */
.builder-form-card__header .builder-form-card__title:last-child {
  margin-bottom: 0;
}

.builder-form-card__title {
  font-size: 1.4rem;
  font-weight: 700;
  color: #1668c9;
  margin-bottom: 0.35rem;
}

.builder-form-card__subtitle {
  font-size: 1.0625rem;
  color: #495862;
}

.builder-form-card .card-body {
  padding: 1.5rem 1.35rem;
}

/* The <hr> between groups inside a card body. Here since the assessment,
   template, course and video builders all use it (they each carried a copy
   until the 2026-10-27 CSS audit). Bootstrap's faint default hr is
   overridden on purpose. */
.builder-form-card__divider {
  margin: 0.85rem 0;
  border: 0;
  border-top: 1px solid #dce0e5;
  opacity: 1;
}

/* The shared notices section (content/_site_notifications.html) on both
   Notifications pages, My work's and the assessment alerts': a plain list
   with light rules between items. Here because two apps' pages include it.
   The space above keeps its heading off My work's header lede, which has no
   bottom margin; the assessment alerts' header has its own, which this
   collapses into. */
.site-notices {
  margin: 1.25rem 0 1.5rem;
}

.site-notices__list {
  margin: 0 0 0.75rem;
  padding: 0;
  list-style: none;
}

.site-notices__item {
  padding: 0.6rem 0;
}

.site-notices__item + .site-notices__item {
  border-top: 1px solid #dee2e6;
}

/* One cover picture (product owner, 2026-10-27): the dropzone is the empty
   state of .cover-editor, and with a photo it floats over the position and
   zoom frame under it (the same box, at the top of .cover-editor), so the
   author sees a single picture with Upload from computer on it, the sliders
   under it and Remove below. The dropzone's label stops catching the pointer
   so the frame can still be dragged, and only the Upload button does. The
   scrim is dropped (it would dim the crop being chosen) for a dark button of
   its own. Used by the course builder, articles, learning paths, the
   assessment builder and the template builder. */
.cover-editor {
  position: relative;
}

.cover-editor .cover-image-dropzone.has-image {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1;
  pointer-events: none;
  overflow: visible;
}

.cover-editor .cover-image-dropzone.has-image .image-dropzone-widget__zone {
  justify-content: flex-end;
  padding-bottom: 1rem;
  background: none;
}

.cover-editor .cover-image-dropzone.has-image .image-dropzone-widget__button {
  pointer-events: auto;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.65);
  border-color: #fff;
}

.cover-editor .cover-image-dropzone.has-image .image-dropzone-widget__button:hover {
  color: #212529;
  background-color: #fff;
}

/* Purely decorative rocket beside a "Ready to publish" / "Not ready to
   publish" heading (articles, learning paths and videos' publish
   checklist, the template builder's Review step). Oversized well past any
   Bootstrap fs-* step and muted/translucent so it reads as flourish, not
   another status icon competing with the checklist's own done/optional/
   incomplete glyphs. Here since four apps use it (articles.css and
   documents.css carried byte-for-byte copies until the 2026-10-27 CSS
   audit). */
.publish-checklist__rocket {
  font-size: 3.5rem;
  color: var(--bs-secondary-color, #6c757d);
  opacity: 0.5;
}

/* Toggled by static/shared/js/publish_checklist.js once every required
   item is done: full-opacity success green, "ready for liftoff" instead of
   the muted at-rest gray above. */
.publish-checklist__rocket--complete {
  color: var(--bs-success, #198754);
  opacity: 1;
}
