/* sidebar-collapse.css — the sidebar's sticky-collapse rules.
 *
 * Served STATICALLY from public/ (NOT bundled) so its href is byte-identical on
 * every route and Astro's ClientRouter keeps it across SPA navigation. Linked in
 * SwissLayout's <head> — see that link's comment for the full why.
 *
 * Plain CSS only: no Tailwind @apply / @layer / theme() (this file bypasses the
 * Vite/Tailwind pipeline). Moved verbatim from global.css's SIDEBAR section.
 */

/* ===========================================
   SIDEBAR STICKY WIDGETS — Desktop only
   Widgets overlap naturally via position:sticky
   with incrementing z-index. Later widgets slide
   up and cover earlier widgets' bodies.
   =========================================== */

/* Sentinels: zero-height markers tracking natural document position */
.widget-sentinel {
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

@media (min-width: 1024px) {
  .sidebar-wrapper {
    min-height: 100%;
    display: flex;
    flex-direction: column;
  }

  .sidebar-widgets {
    flex: 1;
  }

  .sidebar-widget {
    position: sticky;
    background: var(--color-bg, #fff);
    margin-bottom: 2rem;
  }

  .sidebar-widget:not(:has([data-clip-target])) {
    display: none;
  }

  .sidebar-widget.is-stuck {
    overflow: hidden;
    cursor: pointer;
    border-top: 1px solid #e5e5e5;
  }

  /* Jitter-free frontier collapse: the clip runs as a scroll(root) animation on
     the compositor instead of per-frame JS clipPath (which lags one frame →
     jitter). The widget keeps its natural box height; the clip alone resizes what
     shows — from full height down to the preview floor (--preview-h), then holds
     (animation `both` fill) — and the next widget (opaque, higher z-index) slides
     over it to cover it down to the header. No max-height, no overflow, no extra
     state: the clip IS the resize.

     All values come from CSS vars published by SidebarWrapper.measureAndLayout
     (--collapse-start/--collapse-end = the scroll range; --preview-h = the floor,
     derived from the widget's data-collapsed-h / data-min-body-h). They're written
     once per measure (not per frame, not baked), so they track layout shifts
     without going stale — and there are no duplicated literal fallbacks here (the
     data attributes are the single source of truth). Gated by data-css-frontier on
     the container, so removing that attribute restores the pure-JS path. */
  @keyframes frontier-clip {
    from { clip-path: inset(0 0 0 0); }
    to   { clip-path: inset(0 0 calc(100% - var(--preview-h)) 0); }
  }
  /* Counter the sticky-containment push-up at page bottom (lessons §1) on the
     compositor: instead of a per-frame JS transform (which lagged one frame →
     the residual max-scroll jitter), translate the box back DOWN by the exact
     push-up amount as a scroll(root) animation. Push-up is linear in scroll
     (slope 1) from --pushup-start (where the frozen natural box starts overrunning
     the container bottom) to --pushup-max at the page's max scroll. Both vars are
     published per measure by SidebarWrapper.measureAndLayout, so they track layout
     shifts without going stale (§20). Widgets short enough never to overrun get
     --pushup-max: 0 (no-op). Uses `translate` (not `transform`) so it composes with
     `clip-path` on the same element without conflict. */
  @keyframes frontier-pushup {
    from { translate: 0 0; }
    to   { translate: 0 var(--pushup-max); }
  }
  .sidebar-widgets[data-css-frontier] .sidebar-widget.css-frontier.is-stuck {
    animation: frontier-clip linear both, frontier-pushup linear both;
    animation-timeline: scroll(root), scroll(root);
    animation-range: var(--collapse-start) var(--collapse-end),
                     var(--pushup-start) var(--pushup-end);
    /* Freeze the widget BOX at its natural height (constant) while stuck. The
       clip resizes what SHOWS; the box must NOT shrink with it — otherwise the
       box collapses along with the inner (wrong scrollbar ratio: the scroll
       window and its container shrink together) and, for the last widgets in the
       stack, the shorter box lets the following content cascade up (the Social /
       Contact-fields-leaking bug). The JS path does the same via
       widget.style.height = naturalHeight (lessons §5c/§14). --natural-h is the
       measured inner height; the box = inner + header chrome, but freezing the
       box to --natural-h is close enough since the clip governs what's visible. */
    height: var(--natural-h);
  }
  /* The inner is a scroll window that shrinks WITH the clip (max-height animates
     on the same timeline, from the inner's natural height to the preview floor),
     so during the resize the user can scroll-through to peek at clipped content.
     Animating max-height (not snapping to preview) keeps the resize natural. */
  @keyframes frontier-inner {
    from { max-height: var(--inner-h); }
    to   { max-height: var(--preview-h); }
  }
  .sidebar-widgets[data-css-frontier] .sidebar-widget.css-frontier.is-stuck .widget-inner {
    animation: frontier-inner linear both;
    animation-timeline: scroll(root);
    animation-range: var(--collapse-start) var(--collapse-end);
    overflow-y: auto;
  }
  /* Parked = collapsed to just the header strip. Stop the clip animation and
     pin the clip to the collapsed header height (--collapsed-h), NOT the preview
     floor (--preview-h). For middle widgets the next widget covers the extra
     preview height, but the LAST widgets in the stack have nothing below to cover
     them — without this their frozen box would show the full preview (content
     leaking below the header: the Social / Contact bug). Also drop the scrollbar
     (overflow only — layout-neutral, no flicker; JS resets scrollTop on park). */
  .sidebar-widgets[data-css-frontier] .sidebar-widget.css-frontier.is-stuck.is-parked {
    /* Stop the clip animation (clip is pinned statically below) but KEEP the
       push-up animation running — a parked widget at the bottom of the stack is
       exactly where sticky containment pushes hardest, so it still needs the
       counter-translate. Scroll-timeline animations are positional (a pure
       function of scroll), so dropping frontier-clip from the list re-evaluates
       push-up at the same scroll position with no jump. */
    animation: frontier-pushup linear both;
    animation-timeline: scroll(root);
    animation-range: var(--pushup-start) var(--pushup-end);
    clip-path: inset(0 0 calc(100% - var(--collapsed-h)) 0);
  }
  .sidebar-widgets[data-css-frontier] .sidebar-widget.css-frontier.is-stuck.is-parked .widget-inner {
    animation: none;
    max-height: var(--preview-h);
    overflow-y: hidden;
  }

  /* Pinned: the widget the user clicked to expand — and every widget after it —
     drop out of sticky positioning and flow normally, so the pinned widget can
     be read in full and the following widgets stay visible below it. */
  .sidebar-widget.pinned,
  .sidebar-widget.pinned ~ .sidebar-widget {
    position: static !important;
  }

  .dark .sidebar-widget.is-stuck {
    border-top-color: rgba(255, 255, 255, 0.1);
  }

  .sidebar-widget.is-stuck .widget-inner {
    will-change: transform;
    /* Native inner-scroll: when widget is in in-between state, JS sets
       max-height on widget-inner so the user can wheel/touch-scroll inside
       the widget to reveal content hidden by clipPath. Default
       overscroll-behavior lets wheel chain to the page at the inner edges. */
    overflow-x: hidden;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
  }
  .dark .sidebar-widget.is-stuck .widget-inner {
    scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
  }
  .sidebar-widget.is-stuck .widget-inner::-webkit-scrollbar {
    width: 6px;
  }
  .sidebar-widget.is-stuck .widget-inner::-webkit-scrollbar:horizontal {
    display: none;
  }
  .sidebar-widget.is-stuck .widget-inner::-webkit-scrollbar-thumb {
    background: rgba(0, 0, 0, 0.25);
    border-radius: 3px;
  }
  .dark .sidebar-widget.is-stuck .widget-inner::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.25);
  }
  .sidebar-widget.is-stuck .widget-inner::-webkit-scrollbar-track {
    background: transparent;
  }

  /* Parked: only the collapsed header strip is visible. No scrollbar,
     scroll position reset to (0,0) by JS. */
  .sidebar-widget.is-parked .widget-inner {
    overflow: hidden;
    scrollbar-width: none;
  }
  .sidebar-widget.is-parked .widget-inner::-webkit-scrollbar {
    display: none;
  }

  /* ── Stuck collapse: base transitions ── */
  .widget-card {
    transition: padding 0.2s ease, border 0.2s ease;
  }

  .widget-header {
    transition: font-size 0.15s ease, letter-spacing 0.15s ease,
                margin 0.15s ease, color 0.15s ease;
  }

  .photo-card-date,
  .photo-card-ago,
  .rp-card-date,
  .gs-card-category,
  .otd-card-header time {
    transition: opacity 0.15s ease, max-height 0.15s ease;
    max-height: 3rem;
    overflow: hidden;
  }

  .photo-card-label,
  .rp-card-label,
  .gs-card-label,
  #otd-label {
    transition: margin 0.15s ease;
  }

  /* ── Stuck: compact card headers ── */
  .sidebar-widget.is-stuck .photo-card-header,
  .sidebar-widget.is-stuck .rp-card-header,
  .sidebar-widget.is-stuck .gs-card-header,
  .sidebar-widget.is-stuck .otd-card-header {
    padding: 0 0.75rem;
    align-items: center;
    border-bottom: none;
  }

  /* Card-header labels: match .widget-header stuck style exactly */
  .sidebar-widget.is-stuck .photo-card-label,
  .sidebar-widget.is-stuck .rp-card-label,
  .sidebar-widget.is-stuck .gs-card-label,
  .sidebar-widget.is-stuck #otd-label {
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    color: rgba(0, 0, 0, 0.5);
    line-height: 2.25rem;
  }

  .dark .sidebar-widget.is-stuck .photo-card-label,
  .dark .sidebar-widget.is-stuck .rp-card-label,
  .dark .sidebar-widget.is-stuck .gs-card-label,
  .dark .sidebar-widget.is-stuck #otd-label {
    color: rgba(255, 255, 255, 0.5);
  }

  /* Remove card side borders when stuck */
  .sidebar-widget.is-stuck .photo-card,
  .sidebar-widget.is-stuck .rp-card,
  .sidebar-widget.is-stuck .gs-card,
  .sidebar-widget.is-stuck .otd-card,
  .sidebar-widget.is-stuck .skeleton-container {
    border-left: none;
    border-right: none;
    border-bottom: none;
  }

  .sidebar-widget.is-stuck [data-clip-target] {
    transition: background-color 0.15s ease, padding 0.2s ease;
  }

  /* ── Stuck: hide secondary content ── */
  .sidebar-widget.is-stuck .photo-card-date,
  .sidebar-widget.is-stuck .photo-card-ago,
  .sidebar-widget.is-stuck .rp-card-date,
  .sidebar-widget.is-stuck .gs-card-category,
  .sidebar-widget.is-stuck .otd-card-header time {
    opacity: 0;
    max-height: 0;
  }

  .sidebar-widget.is-stuck .gs-category-select {
    display: none;
  }

  /* On This Day: swap label to the dated phrase when collapsed/stuck */
  .otd-label-stuck {
    display: none;
  }
  .sidebar-widget.is-stuck .otd-label-default {
    display: none;
  }
  .sidebar-widget.is-stuck .otd-label-stuck {
    display: inline;
  }

  /* The list body is capped (max-h-[500px], clipped) only while it sits in
     normal document flow. Once stuck, uncap it so the sidebar's single
     scroll can reach every year — otherwise the clip hides older entries. */
  .sidebar-widget.is-stuck .otd-card-body {
    max-height: none;
    overflow: visible;
  }

  .sidebar-widget.is-stuck .photo-card-label,
  .sidebar-widget.is-stuck .rp-card-label,
  .sidebar-widget.is-stuck .gs-card-label,
  .sidebar-widget.is-stuck #otd-label {
    margin: 0;
  }

  /* Social: hide icons when stuck (already in header) */
  .sidebar-widget.is-stuck .social-widget > :not(.widget-header) {
    display: none;
  }

  /* ── Stuck: action buttons — icon only, no chrome ── */
  .sidebar-widget.is-stuck .photo-year-btn,
  .sidebar-widget.is-stuck .rp-shuffle-btn,
  .sidebar-widget.is-stuck .gs-shuffle-btn {
    width: 2rem;
    height: 2rem;
    flex-shrink: 0;
    margin-left: auto;
    position: relative;
    z-index: 2;
    border: none;
    background: transparent;
    color: rgba(0, 0, 0, 0.4);
  }

  .dark .sidebar-widget.is-stuck .photo-year-btn,
  .dark .sidebar-widget.is-stuck .rp-shuffle-btn,
  .dark .sidebar-widget.is-stuck .gs-shuffle-btn {
    color: rgba(255, 255, 255, 0.4);
  }

  /* ── Stuck: widget-card (Tags, etc.) ── */
  .sidebar-widget.is-stuck .widget-card {
    padding: 0 0.75rem;
    border: none;
  }

  .sidebar-widget.is-stuck .widget-header {
    font-size: 0.625rem;
    letter-spacing: 0.2em;
    text-align: left;
    margin: 0;
    color: rgba(0, 0, 0, 0.5);
    white-space: nowrap;
    overflow: hidden;
    line-height: 2.25rem;
  }

  .dark .sidebar-widget.is-stuck .widget-header {
    color: rgba(255, 255, 255, 0.5);
  }

  /* ── Stuck: tooltip repositioned to left ── */
  .sidebar-widget.is-stuck [data-tooltip]::after {
    left: auto;
    right: 100%;
    top: 50%;
    bottom: auto;
    transform: translateY(-50%) translateX(calc(-1 * var(--tooltip-offset)));
  }

  .sidebar-widget.is-stuck [data-tooltip]::before {
    left: auto;
    right: 100%;
    top: 50%;
    bottom: auto;
    transform: translateY(-50%) translateX(calc(-1 * var(--tooltip-offset) + var(--tooltip-arrow)));
    border-bottom-color: transparent;
    border-left-color: var(--tooltip-bg);
  }

  /* ── Stuck: hover highlight ── */
  .sidebar-widget.is-stuck [data-clip-target] {
    transition: background-color 0.08s ease-out, transform 0.08s ease-out;
  }

  .sidebar-widget.is-stuck:hover [data-clip-target] {
    background-color: rgba(0, 0, 0, 0.06);
    transform: translateX(4px);
  }

  .sidebar-widget.is-stuck:active [data-clip-target] {
    background-color: rgba(0, 0, 0, 0.1);
    transform: translateX(2px);
    transition-duration: 0.03s;
  }

  .dark .sidebar-widget.is-stuck:hover [data-clip-target] {
    background-color: rgba(255, 255, 255, 0.08);
  }

  .dark .sidebar-widget.is-stuck:active [data-clip-target] {
    background-color: rgba(255, 255, 255, 0.14);
  }
}
