/*!
 * Naos Hal Publications
 * Extracted from prototype/index.html — do not hand-edit both copies.
 *
 * Everything is scoped under .hal-pubs and driven by the custom properties in
 * the first block, so rebranding for another client is a variable swap, not a
 * rewrite. The active palette is injected as inline CSS by the settings class.
 */

/* ============================================================================
   Naos Hal Publications — design prototype
   Palette lifted from numpex.org: amber #e09900, charcoal #141618, cyan #0cb4ce.
   Everything is driven by the custom properties below, so rebranding this for
   the next client is a single variable block — not a rewrite.
   ========================================================================= */
.hal-pubs,
.hal-pubs *,
.hal-pubs *::before,
.hal-pubs *::after { box-sizing: border-box; }

.hal-pubs {
  --hal-accent:        #e09900;
  --hal-accent-dark:   #b87d00;
  --hal-accent-text:   #9a6a00;   /* AA-safe amber for text on white (4.7:1); brand #e09900 is only 2.4:1 */
  --hal-accent-soft:   #fff6e3;
  --hal-cyan:          #0cb4ce;

  /* Labels that sit ON a filled surface rather than on the page. Tokens, not
     literal #fff, because both fills inverse in the dark scheme. */
  --hal-on-accent:     #ffffff;

  /* The light palette is the default. Values live once, in THE TWO PALETTES
     below; these only choose which set is in play. */
  --hal-on-ink:        var(--l-on-ink);
  --hal-oa:            var(--l-oa);
  --hal-oa-on-ink:     var(--l-oa-on-ink);
  --hal-line-strong:   var(--l-line-strong);
  --hal-shimmer:       var(--l-shimmer);

  --hal-ink:           var(--l-ink);
  --hal-ink-2:         var(--l-ink-2);
  --hal-ink-3:         var(--l-ink-3);
  --hal-ink-4:         var(--l-ink-4);

  --hal-bg:            var(--l-bg);
  --hal-bg-2:          var(--l-bg-2);
  --hal-bg-3:          var(--l-bg-3);
  --hal-line:          var(--l-line);

  --hal-radius:        10px;
  --hal-radius-sm:     6px;
  --hal-gap:           1.25rem;

  /* No title-font token any more: card titles are real <h2>/<h3> elements and
     inherit the theme's own heading styles, so changing the heading font in
     theme options moves ours with it. Everything else inherits the body font
     the same way. Nothing here hardcodes a typeface. */

  --hal-shadow: 0 1px 2px rgba(20,22,24,.05), 0 4px 16px -6px rgba(20,22,24,.10);
  --hal-shadow-lift: 0 2px 4px rgba(20,22,24,.06), 0 12px 32px -10px rgba(20,22,24,.18);
}

/* ----------------------------------------------------------------------------
   THE TWO PALETTES

   Every colour that differs between light and dark lives here once, named. The
   scheme switch and the per-card override below both point at these, so a value
   is never written twice and the two can never drift apart.
   -------------------------------------------------------------------------- */
.hal-pubs {
  --l-ink: #141618;
  --l-ink-2: #3b4045;
  --l-ink-3: #6b7280;
  --l-ink-4: #9aa1a9;
  --l-bg: #ffffff;
  --l-bg-2: #f7f7f8;
  --l-bg-3: #eeeff1;
  --l-line: #e2e4e8;
  --l-line-strong: #d3d7dd;
  --l-accent-text: #9a6a00;   /* AA-safe amber for text on white (4.7:1) */
  --l-accent-soft: #fff6e3;
  --l-on-ink: #ffffff;
  --l-oa: #1a8a4a;
  --l-oa-on-ink: #4ade80;
  --l-shimmer: #f4f5f6;

  --d-ink: #f3f5f7;
  --d-ink-2: #ccd3da;
  --d-ink-3: #9aa4ae;
  --d-ink-4: #79838d;
  /* The card is deliberately LIGHTER than a typical dark section background, so
     the cards read as raised surfaces rather than holes punched in the page. */
  --d-bg: #1e2226;
  --d-bg-2: #262b30;
  --d-bg-3: #2f353b;
  --d-line: #3a4149;
  --d-line-strong: #4a525b;
  /* Derived from the ACTIVE palette, not hardcoded: the settings screen can set
     --hal-accent to cyan or blue, and the dark tones must follow it rather than
     snapping back to amber. The literal on the first line is the fallback for
     browsers without color-mix(); the second line wins everywhere else. */
  --d-accent-text: #f2ae33;
  --d-accent-text: color-mix(in srgb, var(--hal-accent) 72%, #ffffff);
  --d-accent-soft: #3a2c10;
  --d-accent-soft: color-mix(in srgb, var(--hal-accent) 20%, #141618);
  --d-on-ink: #141618;        /* the ink fill is light here, so its label goes dark */
  --d-oa: #4ade80;
  --d-oa-on-ink: #1a8a4a;     /* the pressed pill is light here, so the dot goes deep */
  --d-shimmer: #363c43;
}

/* ============================================================================
   DARK SCHEME
   Opt in per list with scheme="dark".

   This describes the SECTION the list sits in, so it moves everything: the
   heading, filter bar, result count and footer as well as the cards.

   It is a TOKEN SWAP, not a second stylesheet: every colour below the token
   block reads a custom property, so redefining them here re-themes the whole
   component. That is only true as long as nothing hardcodes a literal colour,
   which the build checks.

   Not auto-detected. The obvious triggers are all unreliable - a theme's own
   light/dark flag can be set on a light row (Uncode's own demo rows here do
   exactly that), and measuring the ancestor background in JavaScript means
   painting light cards first and flipping them, which flickers. The editor
   knows what the section looks like; this is a dropdown.
   ========================================================================= */
.hal-pubs[data-scheme="dark"] {
  --hal-ink: var(--d-ink);
  --hal-ink-2: var(--d-ink-2);
  --hal-ink-3: var(--d-ink-3);
  --hal-ink-4: var(--d-ink-4);
  --hal-bg: var(--d-bg);
  --hal-bg-2: var(--d-bg-2);
  --hal-bg-3: var(--d-bg-3);
  --hal-line: var(--d-line);
  --hal-line-strong: var(--d-line-strong);
  --hal-accent-text: var(--d-accent-text);
  --hal-accent-soft: var(--d-accent-soft);
  --hal-on-ink: var(--d-on-ink);
  --hal-oa: var(--d-oa);
  --hal-oa-on-ink: var(--d-oa-on-ink);
  --hal-shimmer: var(--d-shimmer);

  --hal-shadow: 0 1px 2px rgba(0,0,0,.4), 0 4px 16px -6px rgba(0,0,0,.5);
  --hal-shadow-lift: 0 2px 4px rgba(0,0,0,.45), 0 12px 32px -10px rgba(0,0,0,.6);
}

/* ============================================================================
   CARD BACKGROUND
   Set per list with card_bg="…"; the surfaces arrive as --hal-card-* on the
   container, computed server-side.

   Scoped to the CARD, not the container, and that distinction is the whole
   point. The heading, filter bar, count and footer sit on the page, not on the
   card, so they must keep following the section scheme — a dark card dropped
   into a light section used to take the heading with it and make it vanish.
   ========================================================================= */
.hal-pubs[data-card] .hal-card,
.hal-pubs[data-card] .hal-modal {
  --hal-bg: var(--hal-card-bg);
  --hal-bg-2: var(--hal-card-bg-2);
  --hal-bg-3: var(--hal-card-bg-3);
  --hal-line: var(--hal-card-line);
  --hal-line-strong: var(--hal-card-line-strong);
}

/* The ink follows the card colour's own luminance, worked out in PHP, so a dark
   surface never ends up with dark text on it. */
.hal-pubs[data-card="dark"] .hal-card,
.hal-pubs[data-card="dark"] .hal-modal {
  --hal-ink: var(--d-ink);
  --hal-ink-2: var(--d-ink-2);
  --hal-ink-3: var(--d-ink-3);
  --hal-ink-4: var(--d-ink-4);
  --hal-accent-text: var(--d-accent-text);
  --hal-accent-soft: var(--d-accent-soft);
  --hal-on-ink: var(--d-on-ink);
  --hal-oa: var(--d-oa);
  --hal-oa-on-ink: var(--d-oa-on-ink);
  --hal-shimmer: var(--d-shimmer);
}
.hal-pubs[data-card="light"] .hal-card,
.hal-pubs[data-card="light"] .hal-modal {
  --hal-ink: var(--l-ink);
  --hal-ink-2: var(--l-ink-2);
  --hal-ink-3: var(--l-ink-3);
  --hal-ink-4: var(--l-ink-4);
  --hal-accent-text: var(--l-accent-text);
  --hal-accent-soft: var(--l-accent-soft);
  --hal-on-ink: var(--l-on-ink);
  --hal-oa: var(--l-oa);
  --hal-oa-on-ink: var(--l-oa-on-ink);
  --hal-shimmer: var(--l-shimmer);
}

/* Doc-type badges are the one place with 14 hand-picked colour pairs, tuned for
   a white card. Rather than a second table of 14, derive both ends from the
   same --dt: lighten the text, and use a translucent wash of the type colour
   instead of the pale tint.

   The :not([data-card]) guard matters. A badge sits ON A CARD, so it must
   follow the card's own tone, and only fall back to the section scheme when no
   card colour was chosen. Without it, scheme="dark" with a light card painted
   pale text on a pale wash - 1.2:1, which reads as a badge with the opacity
   turned down. */
.hal-pubs[data-scheme="dark"]:not([data-card]) .hal-badge,
.hal-pubs[data-card="dark"] .hal-badge {
  color: color-mix(in srgb, var(--dt) 45%, #ffffff);
  background: color-mix(in srgb, var(--dt) 26%, transparent);
}
/* The no-cover placeholder hatches white over --dt-soft, which is a pale tint
   built for a light card. Same card-first rule as the badges above. */
.hal-pubs[data-scheme="dark"]:not([data-card]) .hal-thumb--none,
.hal-pubs[data-card="dark"] .hal-thumb--none {
  background:
    repeating-linear-gradient(45deg, transparent 0 6px, rgba(255,255,255,.06) 6px 12px),
    color-mix(in srgb, var(--dt) 22%, transparent);
  color: color-mix(in srgb, var(--dt) 45%, #ffffff);
}
/* The modal scrim has to darken further to separate from a dark page. */
.hal-pubs[data-scheme="dark"] .hal-modal::backdrop,
.hal-pubs[data-card="dark"] .hal-modal::backdrop { background: rgba(0,0,0,.72); }
/* ...but a LIGHT card inside a dark section gets the lighter scrim back, so the
   dialog does not float on a near-black field it no longer matches. */
.hal-pubs[data-card="light"] .hal-modal::backdrop { background: rgba(20,22,24,.55); }

/* Query container: the filter bar responds to the width of the COLUMN it sits
   in, not the viewport. Inside a narrow theme column a viewport breakpoint
   fires far too late. */
.hal-pubs { container-type: inline-size; }

/* ============================================================================
   FILTER BAR
   ========================================================================= */
/* The bar has exactly TWO flex children: the search box and .hal-filterbar.
   With only two items, flex-wrap can produce only two outcomes - both on one
   line, or the filters drop as a complete row. That removes the ragged
   in-between states you get when five separate pills wrap one at a time.
   No breakpoint needed: the break is driven by available width. */
.hal-controls {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  padding: .6rem .65rem;
  margin-bottom: 1rem;
  box-shadow: var(--hal-shadow);
}
.hal-search {
  /* Grows to fill line one once the filters have wrapped below it.
     min-width is deliberately small: the search box absorbs ALL the shrinking
     so the bar never wraps of its own accord above the container-query
     threshold. Wrapping is triggered by the query alone, which is what keeps
     the wrap and the stretch in lockstep. */
  flex: 1 1 260px;
  min-width: 100px;
  position: relative;
  display: flex;
  align-items: center;
}

/* All filter controls travel together as one unit. flex: 0 0 auto means the
   group never shrinks, so the search box takes all the compression and the
   pills keep their readable width. */
.hal-filterbar {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 0 0 auto;
  flex-wrap: wrap;
}
.hal-pubs .hal-search input {
  width: 100%;
  font: inherit;
  font-size: .875rem;
  /* Explicit height, not padding-derived: the pills are a fixed 34px and a
     padding-sized input can never line up with them across themes. */
  height: 34px;
  line-height: 1;
  margin: 0;
  padding: 0 .7rem 0 2rem;
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius-sm);
  background: var(--hal-bg-2);
  color: var(--hal-ink);
}
.hal-search input:focus {
  outline: none;
  border-color: var(--hal-accent);
  background: var(--hal-bg);
  box-shadow: 0 0 0 3px rgba(224,153,0,.15);
}
.hal-pubs .hal-search svg {
  position: absolute; left: .7rem; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--hal-ink-4); pointer-events: none;
}
/* ---- pill controls: dropdown triggers, the OA toggle and the sort <select>
   all share one visual spec so the bar reads as a single row of controls. --- */
.hal-pubs .hal-pill,
.hal-pubs .hal-sort select {
  font: inherit; font-size: .82rem; font-weight: 600;
  display: inline-flex; align-items: center; gap: .4rem;
  height: 34px;
  padding: 0 .7rem;
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius-sm);
  background: var(--hal-bg-2);
  color: var(--hal-ink-2);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color .15s, background .15s, color .15s;
}
.hal-pill:hover, .hal-sort select:hover { border-color: var(--hal-ink-4); }
.hal-pill:focus-visible, .hal-sort select:focus-visible {
  outline: 2px solid var(--hal-accent); outline-offset: 2px;
}
.hal-pill svg.caret { width: 11px; height: 11px; opacity: .55; transition: transform .18s; }
.hal-pill[aria-expanded="true"] { border-color: var(--hal-ink-3); background: var(--hal-bg-3); }
.hal-pill[aria-expanded="true"] svg.caret { transform: rotate(180deg); }

/* count badge shown when the dropdown has active selections */
.hal-pubs .hal-pill .cnt {
  font-size: .7rem; font-weight: 800; font-variant-numeric: tabular-nums;
  min-width: 17px; height: 17px; padding: 0 .25rem;
  display: none; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--hal-accent); color: var(--hal-ink);
}
.hal-pubs .hal-pill.is-active { border-color: var(--hal-accent); background: var(--hal-accent-soft); color: var(--hal-accent-dark); }
.hal-pill.is-active .cnt { display: inline-flex; }

/* binary toggle (open access) */
.hal-pubs .hal-pill[aria-pressed="true"] {
  background: var(--hal-ink); border-color: var(--hal-ink); color: var(--hal-on-ink);
}
.hal-pill[aria-pressed="true"] .dotmark { background: var(--hal-oa-on-ink); }
.dotmark {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--hal-ink-4); flex: none;
}

.hal-sort select {
  padding-right: 1.9rem;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .6rem center;
}

/* ---- dropdown panel ------------------------------------------------------ */
.hal-dd { position: relative; }
.hal-ddpanel {
  position: absolute; z-index: 60; top: calc(100% + 6px); left: 0;
  min-width: 240px; max-width: 320px;
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  box-shadow: 0 12px 32px -8px rgba(20,22,24,.22), 0 2px 6px rgba(20,22,24,.06);
  overflow: hidden;
  animation: hal-dd-in .13s ease-out;
}
.hal-ddpanel[hidden] { display: none; }
@keyframes hal-dd-in { from { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .hal-ddpanel { animation: none; } }
/* flip to right edge when the trigger is near the viewport edge */
.hal-dd.flip .hal-ddpanel { left: auto; right: 0; }

.hal-ddsearch { padding: .55rem .6rem; border-bottom: 1px solid var(--hal-line); }
.hal-pubs .hal-ddsearch input {
  width: 100%; font: inherit; font-size: .82rem;
  padding: .35rem .55rem;
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius-sm);
  background: var(--hal-bg-2); color: var(--hal-ink);
}
.hal-ddsearch input:focus { outline: none; border-color: var(--hal-accent); background: var(--hal-bg); }

.hal-ddlist { max-height: 264px; overflow-y: auto; overscroll-behavior: contain; padding: .3rem; margin: 0; list-style: none; }
.hal-ddlist li { margin: 0; }
.hal-ddlist li[hidden] { display: none; }
.hal-pubs .hal-ddopt {
  display: flex; align-items: center; gap: .55rem;
  padding: .38rem .5rem;
  border-radius: var(--hal-radius-sm);
  font-size: .82rem; color: var(--hal-ink-2);
  cursor: pointer;
}
.hal-ddopt:hover { background: var(--hal-bg-2); }
.hal-ddopt input { accent-color: var(--hal-accent); width: 14px; height: 14px; flex: none; cursor: pointer; }
.hal-ddopt .lbl { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hal-pubs .hal-ddopt .n {
  font-size: .72rem; font-variant-numeric: tabular-nums;
  color: var(--hal-ink-4); background: var(--hal-bg-3);
  padding: .05rem .35rem; border-radius: 999px; flex: none;
}
.hal-pubs .hal-ddopt:has(input:checked) { background: var(--hal-accent-soft); color: var(--hal-accent-dark); font-weight: 600; }

.hal-pubs .hal-ddfoot {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  padding: .45rem .6rem;
  border-top: 1px solid var(--hal-line);
  background: var(--hal-bg-2);
  font-size: .74rem; color: var(--hal-ink-4);
}
.hal-pubs .hal-ddfoot button {
  font: inherit; font-size: .74rem; font-weight: 700;
  background: none; border: 0; padding: 0;
  color: var(--hal-accent-dark); cursor: pointer;
  border-bottom: 1px solid currentColor;
}
.hal-pubs .hal-ddempty { padding: 1rem .6rem; text-align: center; font-size: .8rem; color: var(--hal-ink-4); }

.hal-resultbar {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  margin-bottom: 1rem;
}
.hal-pubs .hal-count { font-size: .92rem; color: var(--hal-ink-3); }
.hal-pubs .hal-count strong { color: var(--hal-ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.hal-pubs .hal-clear {
  font: inherit; font-size: .82rem; font-weight: 600;
  background: none; border: 0; padding: .2rem 0;
  color: var(--hal-accent-dark);
  cursor: pointer;
  border-bottom: 1px solid currentColor;
}
.hal-clear[hidden] { display: none; }

/* ============================================================================
   DOC-TYPE COLOUR TOKENS
   Colour is never the only signal — every badge also carries its label.
   ========================================================================= */
.hal-card { --dt: var(--hal-ink-3); --dt-soft: var(--hal-bg-3); }
.hal-card[data-type="ART"]         { --dt:#1e6fd9; --dt-soft:#e8f1fd; }
.hal-card[data-type="COMM"]        { --dt:#7c4dcc; --dt-soft:#f1ebfd; }
.hal-card[data-type="UNDEFINED"]   { --dt:#c07800; --dt-soft:#fff4e0; }
.hal-card[data-type="POSTER"]      { --dt:#c2367d; --dt-soft:#fdeaf3; }
.hal-card[data-type="REPORT"]      { --dt:#0f8c8c; --dt-soft:#e2f5f5; }
.hal-card[data-type="PROCEEDINGS"] { --dt:#4b56c4; --dt-soft:#ebedfc; }
.hal-card[data-type="THESE"]       { --dt:#c0392b; --dt-soft:#fdecea; }
.hal-card[data-type="HDR"]         { --dt:#96281b; --dt-soft:#fbe9e6; }
.hal-card[data-type="MEM"]         { --dt:#5a6472; --dt-soft:#eef0f3; }
.hal-card[data-type="SOFTWARE"]    { --dt:#1a8a4a; --dt-soft:#e5f6ec; }
.hal-card[data-type="OUV"]         { --dt:#8a5a1a; --dt-soft:#f7efe3; }
.hal-card[data-type="COUV"]        { --dt:#a06a2c; --dt-soft:#f9f1e6; }
.hal-card[data-type="IMG"],
.hal-card[data-type="VIDEO"],
.hal-card[data-type="SON"]         { --dt:#0cb4ce; --dt-soft:#e3f7fb; }

/* Base rule: .hal-metaline is a <span>, so it needs an explicit display. */
.hal-metaline { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

.hal-pubs .hal-badge {
  display: inline-flex; align-items: center;
  font-size: .66rem; font-weight: 800;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--dt);
  background: var(--dt-soft);
  padding: .24rem .5rem;
  border-radius: 4px;
  white-space: nowrap;
}
.hal-pubs .hal-oa {
  display: inline-flex; align-items: center; gap: .25rem;
  font-size: .68rem; font-weight: 700;
  color: var(--hal-oa);
  white-space: nowrap;
}
.hal-oa svg { width: 11px; height: 11px; }
.hal-pubs .hal-dot { color: var(--hal-line); }
.hal-pubs .hal-year { font-size: .74rem; font-weight: 600; color: var(--hal-ink-3); font-variant-numeric: tabular-nums; }

/* ---- shared card bits ---------------------------------------------------- */
.hal-pubs .hal-title {
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--hal-ink);
  margin: 0;
}
.hal-pubs .hal-title a { color: inherit; text-decoration: none; }
.hal-title a:hover { text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--hal-accent); }
.hal-title a:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 3px; border-radius: 2px; }

.hal-pubs .hal-authors { font-size: .84rem; color: var(--hal-ink-2); line-height: 1.5; }
.hal-pubs .hal-authors .more { color: var(--hal-ink-4); }
.hal-pubs .hal-venue { font-size: .82rem; color: var(--hal-ink-3); line-height: 1.5; }
.hal-pubs .hal-venue em { font-style: italic; color: var(--hal-ink-2); }

.hal-kw { display: flex; flex-wrap: wrap; gap: .3rem; }
.hal-pubs .hal-kw span {
  font-size: .7rem;
  color: var(--hal-ink-3);
  background: var(--hal-bg-2);
  border: 1px solid var(--hal-line);
  padding: .16rem .45rem;
  border-radius: 4px;
}
.hal-links { display: flex; flex-wrap: wrap; gap: .4rem; }
.hal-pubs .hal-btn {
  display: inline-flex; align-items: center; gap: .32rem;
  font-size: .78rem; font-weight: 600;
  text-decoration: none;
  padding: .35rem .68rem;
  border-radius: var(--hal-radius-sm);
  border: 1px solid var(--hal-line);
  color: var(--hal-ink-2);
  background: var(--hal-bg);
  transition: border-color .15s, background .15s, color .15s;
}
.hal-btn:hover { border-color: var(--hal-ink-3); background: var(--hal-bg-2); }
.hal-btn:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 2px; }
.hal-btn svg { width: 12px; height: 12px; }
/* Read PDF is the primary action: solid at rest, dropping to the outline on
   hover - the inverse of the secondary buttons. */
.hal-pubs .hal-btn--primary {
  background: var(--hal-accent);
  border-color: var(--hal-accent);
  color: var(--hal-on-accent);
  font-weight: 700;
}
.hal-pubs .hal-btn--primary:hover,
.hal-pubs .hal-btn--primary:focus-visible {
  background: var(--hal-bg);
  border-color: var(--hal-accent);
  color: var(--hal-accent);
}

/* ============================================================================
   TREATMENT 1 — EDITORIAL CARDS (cover thumbnails)
   ========================================================================= */
[data-layout^="editorial"] .hal-list { display: grid; gap: var(--hal-gap); }
[data-layout^="editorial"] .hal-card {
  display: grid;
  grid-template-columns: 116px 1fr;
  gap: 1.15rem;
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  padding: 1.15rem;
  box-shadow: var(--hal-shadow);
  transition: box-shadow .2s, border-color .2s, transform .2s;
}
[data-layout^="editorial"] .hal-card:hover {
  box-shadow: var(--hal-shadow-lift);
  border-color: var(--hal-line-strong);
  transform: translateY(-1px);
}
[data-layout^="editorial"] .hal-thumb {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--hal-radius-sm);
  overflow: hidden;
  background: var(--dt-soft);
  border: 1px solid var(--hal-line);
}
[data-layout^="editorial"] .hal-thumb img {
  width: 100%; height: 100%; object-fit: cover; object-position: top center;
  display: block;
}
/* fallback tile when thumbId_i is absent or the image 404s */
[data-layout^="editorial"] .hal-thumb--none {
  display: grid; place-items: center;
  background:
    repeating-linear-gradient(45deg, transparent 0 6px, rgba(255,255,255,.5) 6px 12px),
    var(--dt-soft);
}
.hal-pubs[data-layout^="editorial"] .hal-thumb--none span {
  font-size: .62rem; font-weight: 800; letter-spacing: .08em;
  color: var(--dt); text-transform: uppercase; text-align: center; padding: .3rem;
}
[data-layout^="editorial"] .hal-body { display: flex; flex-direction: column; gap: .5rem; min-width: 0; }
[data-layout^="editorial"] .hal-metaline { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
[data-layout^="editorial"] .hal-title { font-size: 1.08rem; line-height: 1.33; }
[data-layout^="editorial"] .hal-links { margin-top: .3rem; }

@media (max-width: 620px) {
  [data-layout^="editorial"] .hal-card { grid-template-columns: 76px 1fr; gap: .85rem; padding: .95rem; }
  [data-layout^="editorial"] .hal-title { font-size: 1rem; }
}

/* Cover image is optional — shortcode attr thumbnails="yes|no".
   With it off the card collapses to a single column, no empty gutter. */
[data-layout^="editorial"][data-thumbs="off"] .hal-card { grid-template-columns: 1fr; }
[data-layout^="editorial"][data-thumbs="off"] .hal-thumb { display: none; }

/* ============================================================================
   TREATMENT 4 — EDITORIAL + "LEARN MORE" MODAL
   Same card as treatment 1, plus a button that opens the treatment-3 detail
   block in a dialog. Card stays scannable; depth is one click away.
   ========================================================================= */
/* Sits immediately after the other actions in the row — not pushed to the
   card edge, so the buttons stay one visual group. */
.hal-pubs .hal-btn--more {
  border-color: var(--hal-accent);
  /* Brand amber, not the darkened variant: the client asked for these three
     accent labels - Learn more, Read PDF on hover, and the bottom CTA on
     hover - to match the brand colour exactly. Small TEXT links elsewhere
     (Clear all filters, the dropdown Clear) keep --hal-accent-text, which is
     the AA-safe amber. */
  color: var(--hal-accent);
  background: var(--hal-accent-soft);
}
.hal-pubs .hal-btn--more:hover,
.hal-pubs .hal-btn--more:focus-visible { background: var(--hal-accent); color: var(--hal-on-accent); border-color: var(--hal-accent); }

/* Native <dialog>: focus trapping, Esc-to-close, background inert — all free. */
.hal-pubs .hal-modal {
  width: min(720px, calc(100vw - 2rem));
  max-height: min(85vh, 900px);
  padding: 0;
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  box-shadow: 0 24px 64px -12px rgba(20,22,24,.35);
  color: var(--hal-ink);
  background: var(--hal-bg);
  overflow: hidden;
}
.hal-modal::backdrop {
  background: rgba(20,22,24,.55);
  backdrop-filter: blur(3px);
}
/* Flex column ONLY when open — a bare `display:flex` would override the UA's
   `display:none` on a closed dialog and leave it permanently visible.
   This is also what lets .hal-modal-body take a bounded height and scroll;
   without it the body grows past max-height and gets clipped unreachably. */
.hal-modal[open] {
  display: flex;
  flex-direction: column;
  animation: hal-pop .18s ease-out;
}
@keyframes hal-pop { from { opacity: 0; transform: translateY(8px) scale(.985); } }

.hal-modal-head {
  flex: 0 0 auto;
  display: grid; grid-template-columns: 1fr auto; gap: .6rem 1rem;
  align-items: start;
  padding: 1.25rem 1.35rem 1rem;
  background: var(--hal-bg);
  border-bottom: 1px solid var(--hal-line);
}
.hal-modal-head .hal-metaline { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.hal-modal-head .hal-title { font-size: 1.22rem; line-height: 1.3; }
.hal-pubs .hal-modal-close {
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--hal-line);
  border-radius: 50%;
  background: var(--hal-bg-2);
  color: var(--hal-ink-2);
  cursor: pointer;
}
.hal-pubs .hal-modal-close:hover { background: var(--hal-ink); color: var(--hal-on-ink); border-color: var(--hal-ink); }
.hal-modal-close:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 2px; }
.hal-modal-close svg { width: 15px; height: 15px; }

.hal-modal-body {
  flex: 1 1 auto;
  min-height: 0;              /* without this a flex child refuses to shrink and never scrolls */
  overflow-y: auto;
  overscroll-behavior: contain; /* stops scroll chaining to the page behind */
  -webkit-overflow-scrolling: touch;
  padding: 1.25rem 1.35rem 1.5rem;
}
.hal-modal-body .hal-authors { font-size: .875rem; }
/* Fade hint that there is more below the fold. */
.hal-modal-body::after {
  content: ""; display: block; height: .25rem;
}

@media (prefers-reduced-motion: reduce) {
  .hal-modal[open] { animation: none; }
}

/* ============================================================================
   TREATMENT 5 — COMPACT + "LEARN MORE" MODAL
   Treatment 4 stripped back: no cover, no keyword chips, flat cards with no
   shadow or hover lift. Depth lives entirely in the modal, so the list stays
   quiet and scannable.
   ========================================================================= */
[data-layout="compact"] .hal-list { display: grid; --hal-gap: .5rem; gap: var(--hal-gap); }
/* With a cover the card becomes two columns; without one it stays a single
   column and the body fills it. */
[data-layout="compact"][data-thumbs="on"] .hal-card:has(.hal-thumb),
[data-layout="list"][data-thumbs="on"] .hal-card:has(.hal-thumb) {
  display: grid; grid-template-columns: 76px 1fr; gap: 1rem; align-items: start;
}
[data-layout="compact"] .hal-thumb,
[data-layout="list"] .hal-thumb {
  display: block; aspect-ratio: 3 / 4; border-radius: var(--hal-radius-sm);
  overflow: hidden; background: var(--dt-soft); border: 1px solid var(--hal-line);
}
[data-layout="compact"] .hal-thumb img,
[data-layout="list"] .hal-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
[data-layout="compact"] .hal-thumb--none,
[data-layout="list"] .hal-thumb--none { display: grid; place-items: center; background: repeating-linear-gradient(45deg, transparent 0 6px, rgba(255,255,255,.5) 6px 12px), var(--dt-soft); }
.hal-pubs[data-layout="compact"] .hal-thumb--none span,
.hal-pubs[data-layout="list"] .hal-thumb--none span { font-size: .58rem; font-weight: 800; letter-spacing: .08em; color: var(--dt); text-transform: uppercase; text-align: center; padding: .25rem; }
[data-layout="compact"] .hal-body,
[data-layout="list"] .hal-body { display: flex; flex-direction: column; gap: .4rem; min-width: 0; }

[data-layout="compact"] .hal-card {
  display: flex; flex-direction: column; gap: .4rem;
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius-sm);
  padding: .9rem 1.1rem;
  transition: border-color .15s, background .15s;
}
[data-layout="compact"] .hal-card:hover { background: var(--hal-bg-2); }
[data-layout="compact"] .hal-title { font-size: 1rem; line-height: 1.35; }
[data-layout="compact"] .hal-links { margin-top: .3rem; }

/* ============================================================================
   TREATMENT 2 — TYPOGRAPHIC LIST (no images)
   ========================================================================= */
[data-layout="list"] .hal-list {
  /* Cards butt together and are separated by a rule, not a gap. Stated as the
     token anyway: the carousel sizes its track from --hal-gap, and a layout
     that leaves it at the default would reserve space it never uses. */
  --hal-gap: 0px;
  gap: var(--hal-gap);
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  box-shadow: var(--hal-shadow);
  overflow: hidden;
}
[data-layout="list"] .hal-card {
  display: flex; flex-direction: column; gap: .42rem;
  padding: 1.15rem 1.25rem;
  transition: background .15s;
}
[data-layout="list"] .hal-card + .hal-card { border-top: 1px solid var(--hal-line); }
[data-layout="list"] .hal-card:hover { background: var(--hal-bg-2); }
[data-layout="list"] .hal-metaline { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
[data-layout="list"] .hal-title { font-size: 1.02rem; line-height: 1.35; }
[data-layout="list"] .hal-links { margin-top: .25rem; }
.hal-pubs[data-layout="list"] .hal-btn {
  border: 0; padding: .2rem 0; margin-right: .95rem; background: none;
  color: var(--hal-accent-dark); border-radius: 0;
}
[data-layout="list"] .hal-btn:hover { background: none; text-decoration: underline; text-underline-offset: 3px; }
.hal-pubs[data-layout="list"] .hal-btn--primary { background: none; border: 0; color: var(--hal-accent-text); }
.hal-pubs[data-layout="list"] .hal-btn--primary:hover { background: none; color: var(--hal-ink); }

/* ============================================================================
   TREATMENT 3 — EXPANDABLE CARDS
   ========================================================================= */
[data-layout="expand"] .hal-list { display: grid; --hal-gap: .55rem; gap: var(--hal-gap); }
/* Head row: optional cover beside the body, detail spans the full width below. */
[data-layout="expand"] .hal-head { display: grid; grid-template-columns: 1fr auto; gap: 1.15rem; padding: 1rem 1.15rem; align-items: start; }
[data-layout="expand"][data-thumbs="on"] .hal-head:has(.hal-thumb) { grid-template-columns: 96px 1fr auto; }
[data-layout="expand"] .hal-body { display: flex; flex-direction: column; gap: .45rem; min-width: 0; }
[data-layout="expand"] .hal-thumb { display: block; position: relative; aspect-ratio: 3 / 4; border-radius: var(--hal-radius-sm); overflow: hidden; background: var(--dt-soft); border: 1px solid var(--hal-line); }
[data-layout="expand"] .hal-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
[data-layout="expand"] .hal-thumb--none { display: grid; place-items: center; background: repeating-linear-gradient(45deg, transparent 0 6px, rgba(255,255,255,.5) 6px 12px), var(--dt-soft); }
.hal-pubs[data-layout="expand"] .hal-thumb--none span { font-size: .62rem; font-weight: 800; letter-spacing: .08em; color: var(--dt); text-transform: uppercase; text-align: center; padding: .3rem; }
[data-layout="expand"] .hal-card {
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  box-shadow: var(--hal-shadow);
  overflow: hidden;
}
/* Round chevron, top right. It used to wrap the whole card, which made its
   accessible name the entire card text and hid the title from heading
   navigation; now it is icon-only with a visually hidden label. */
.hal-pubs[data-layout="expand"] .hal-summary {
  width: 28px; height: 28px; flex: none;
  display: grid; place-items: center;
  padding: 0; border: 0;
  border-radius: 50%;
  background: var(--hal-bg-2);
  color: var(--hal-ink-3);
  cursor: pointer;
  transition: background .15s, color .15s;
}
.hal-pubs[data-layout="expand"] .hal-summary:hover { background: var(--hal-bg-3); color: var(--hal-ink); }
[data-layout="expand"] .hal-summary:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 2px; }
[data-layout="expand"] .hal-title { font-size: 1rem; line-height: 1.35; }

.hal-pubs[data-layout="expand"] .hal-caret {
  display: grid; place-items: center;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--hal-bg-2);
  color: var(--hal-ink-3);
  transition: transform .22s, background .15s;
}
[data-layout="expand"] .hal-caret svg { width: 12px; height: 12px; }
.hal-pubs[data-layout="expand"] .hal-summary[aria-expanded="true"] { background: var(--hal-accent); color: var(--hal-ink); }
[data-layout="expand"] .hal-summary[aria-expanded="true"] .hal-caret { transform: rotate(180deg); }
/* Detail block — shared by the expandable card and the modal, so both stay
   identical by construction rather than by discipline. */
.hal-detail { display: flex; flex-direction: column; gap: .85rem; }
.hal-detail[hidden] { display: none; }
.hal-pubs .hal-detail h4 {
  margin: 1rem 0 .3rem;
  font-size: .66rem; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--hal-ink-4);
}
.hal-detail > div:first-child h4,
.hal-detail > h4:first-child { margin-top: 0; }
.hal-pubs .hal-abstract {
  margin: 0; font-size: .875rem; line-height: 1.65; color: var(--hal-ink-2);
  max-width: 74ch;
}
.hal-pubs .hal-labs { font-size: .82rem; color: var(--hal-ink-3); line-height: 1.55; }

[data-layout="expand"] .hal-detail {
  /* The first heading inside carries margin-top:0, so without padding here it
     would sit flush against the border. */
  padding: .95rem 1.15rem 1.15rem;
  border-top: 1px solid var(--hal-line);
}

/* ---- states -------------------------------------------------------------- */
.hal-pubs .hal-state { padding: 3.5rem 1rem; text-align: center; color: var(--hal-ink-3); }
.hal-pubs .hal-state h3 { margin: 0 0 .4rem; font-size: 1rem; color: var(--hal-ink); }
.hal-state p { margin: 0; font-size: .88rem; }
.hal-skeleton { display: grid; gap: var(--hal-gap); }
.hal-skeleton div {
  height: 128px;
  border-radius: var(--hal-radius);
  background: linear-gradient(90deg, var(--hal-bg-3) 25%, var(--hal-shimmer) 37%, var(--hal-bg-3) 63%);
  background-size: 400% 100%;
  animation: hal-shimmer 1.3s ease-in-out infinite;
}
@keyframes hal-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) {
  .hal-skeleton div { animation: none; }
  .hal-card, .hal-caret { transition: none !important; }
}

.hal-more { display: grid; place-items: center; margin-top: 1.5rem; }
.hal-pubs .hal-more button {
  font: inherit; font-size: .88rem; font-weight: 600;
  padding: .68rem 1.6rem;
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius-sm);
  background: var(--hal-bg);
  color: var(--hal-ink);
  cursor: pointer;
  box-shadow: var(--hal-shadow);
}
.hal-more button:hover { border-color: var(--hal-ink-3); }
.hal-more button:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 2px; }
.hal-more button[disabled] { opacity: .5; cursor: default; }

/* Bottom call to action, out to the HAL collection itself.
   Deliberately NOT .hal-btn: those are 12px card actions, the typographic list
   flattens them into plain text links, and the Uncode colour override that
   keeps them in the theme's ink would fight this one's fill. It borrows the
   Read PDF language instead - solid at rest, dropping to the outline on hover -
   because that is the primary-action pattern already approved here. */
.hal-cta { display: grid; place-items: center; margin-top: 2rem; }
.hal-pubs .hal-cta a {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .9rem; font-weight: 700;
  text-decoration: none;
  padding: .78rem 1.7rem;
  border-radius: var(--hal-radius-sm);
  border: 1px solid var(--hal-accent);
  background: var(--hal-accent);
  color: var(--hal-on-accent);
  transition: background .15s, color .15s, border-color .15s;
}
.hal-pubs .hal-cta a:hover,
.hal-pubs .hal-cta a:focus-visible {
  background: var(--hal-bg);
  color: var(--hal-accent);
}
.hal-cta a:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 2px; }
.hal-cta a svg { width: 14px; height: 14px; }

.hal-pubs .hal-footer {
  margin-top: 2.5rem; padding-top: 1.15rem;
  border-top: 1px solid var(--hal-line);
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  font-size: .78rem; color: var(--hal-ink-4);
}
.hal-pubs .hal-footer a { color: var(--hal-ink-3); }

/* ============================================================================
   TREATMENT 6 — CARD GRID
   Tiles several across: cover on top, metadata beneath, actions pinned to the
   bottom edge so the buttons line up across a row however long the titles are.

   Deliberately reuses the editorial card MARKUP rather than introducing a
   sixth one. The PHP renderer and its JavaScript twin have to stay
   byte-identical, and every extra card shape doubles that surface. Here the
   difference is entirely layout, so CSS carries it alone.
   ========================================================================= */
[data-layout="grid"] .hal-list {
  display: grid;
  gap: var(--hal-gap);
}
/* Column tracks are for the ordinary grid ONLY. A carousel builds its own
   track, and any grid-template-columns landing on it puts the first card in an
   explicit track sized by the leftover space - which is nothing, because the
   implicit tracks already used it all. The card collapses to a couple of
   pixels, its text wraps one character per line, and the row it stretches
   drags every sibling to thousands of pixels tall. That is not a theoretical
   risk: it is what shipped, and :not([data-carousel]) is what prevents it. */
[data-layout="grid"]:not([data-carousel]) .hal-list {
  /* auto-fill by default. min(100%, 250px) stops a single tile overflowing a
     container narrower than the track minimum. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr));
}
/* A fixed count, when the editor wants the rows to line up predictably.
   minmax(0, 1fr) rather than 1fr: a long unbroken DOI or title would otherwise
   push the track wider than its share. */
[data-layout="grid"]:not([data-carousel])[data-cols="1"] .hal-list { grid-template-columns: minmax(0, 1fr); }
[data-layout="grid"]:not([data-carousel])[data-cols="2"] .hal-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
[data-layout="grid"]:not([data-carousel])[data-cols="3"] .hal-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
[data-layout="grid"]:not([data-carousel])[data-cols="4"] .hal-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }

[data-layout="grid"] .hal-card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--hal-bg);
  border: 1px solid var(--hal-line);
  border-radius: var(--hal-radius);
  overflow: hidden;            /* lets the cover bleed to the tile edge */
  box-shadow: var(--hal-shadow);
  transition: box-shadow .2s, border-color .2s, transform .2s;
}
[data-layout="grid"] .hal-card:hover {
  box-shadow: var(--hal-shadow-lift);
  border-color: var(--hal-line-strong);
  transform: translateY(-1px);
}

/* ---- tile size --------------------------------------------------------
   Three sizes for the grid tile, driven by one set of tokens so the cover,
   the padding and the type stay in proportion to each other.

   The default tile is 528px tall at a 400px width - 59% of a 900px viewport,
   which means a visitor has to scroll the page to line one up before they can
   read it. That is fine for a wall of tiles several across, where the height is
   shared out; it is not fine for a single-column carousel, which is where this
   came from. Small brings it to roughly a third of the viewport.

   Scoped to the card grid, like the column count: the other templates put the
   cover beside the text rather than above it, so there is no tile to size. */
.hal-pubs {
  --hal-tile-cover: 240px;
  --hal-tile-pad-y: 1rem;
  --hal-tile-pad-x: 1.05rem;
  --hal-tile-gap: .5rem;
  --hal-tile-title: 1rem;
  --hal-tile-meta: .82rem;
}
.hal-pubs[data-card-size="small"] {
  --hal-tile-cover: 112px;
  --hal-tile-pad-y: .7rem;
  --hal-tile-pad-x: .8rem;
  --hal-tile-gap: .3rem;
  --hal-tile-title: .9rem;
  --hal-tile-meta: .76rem;
}
.hal-pubs[data-card-size="large"] {
  --hal-tile-cover: 320px;
  --hal-tile-pad-y: 1.2rem;
  --hal-tile-pad-x: 1.3rem;
  --hal-tile-gap: .6rem;
  --hal-tile-title: 1.08rem;
  --hal-tile-meta: .86rem;
}

[data-layout="grid"] .hal-thumb {
  position: relative;
  /* 4/3 keeps narrow tiles from looking like letterboxed strips, but a wide
     tile at that ratio produces a 400px-tall cover that swamps the metadata,
     so the height is capped and the image crops instead. */
  aspect-ratio: 4 / 3;
  max-height: var(--hal-tile-cover);
  overflow: hidden;
  background: var(--dt-soft);
  border-bottom: 1px solid var(--hal-line);
}
[data-layout="grid"] .hal-thumb img {
  width: 100%; height: 100%; object-fit: cover;
  /* The useful part of a PDF first page is the title block at the top. */
  object-position: top center;
  display: block;
}
[data-layout="grid"] .hal-thumb--none {
  display: grid; place-items: center;
  background:
    repeating-linear-gradient(45deg, transparent 0 6px, rgba(255,255,255,.5) 6px 12px),
    var(--dt-soft);
}
.hal-pubs[data-layout="grid"] .hal-thumb--none span {
  font-size: .62rem; font-weight: 800; letter-spacing: .08em;
  color: var(--dt); text-transform: uppercase; text-align: center; padding: .3rem;
}

[data-layout="grid"] .hal-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--hal-tile-pad-y) var(--hal-tile-pad-x) calc(var(--hal-tile-pad-y) + .1rem);
  gap: var(--hal-tile-gap);
  flex: 1;                     /* so margin-top:auto below can reach the floor */
}
[data-layout="grid"] .hal-metaline { display: flex; align-items: center; gap: var(--hal-tile-gap); flex-wrap: wrap; }
[data-layout="grid"] .hal-title { font-size: var(--hal-tile-title); line-height: 1.35; }
/* The supporting lines scale too, or a small tile keeps default-sized author
   and venue text and barely shrinks at all - most of the height is here. */
[data-layout="grid"] .hal-authors,
[data-layout="grid"] .hal-venue { font-size: var(--hal-tile-meta); }
[data-layout="grid"] .hal-links { margin-top: auto; padding-top: calc(var(--hal-tile-gap) * .7); }

/* Cover image is optional here too — shortcode attr thumbnails="yes|no". */
[data-layout="grid"][data-thumbs="off"] .hal-thumb { display: none; }

/* ============================================================================
   CAROUSEL
   A modifier on the track, not a seventh template: any layout can become one,
   because the cards are untouched and only .hal-list changes. Same reasoning as
   the card grid — the PHP renderer and its JavaScript twin stay one pair.

   Built on CSS scroll-snap rather than a carousel library. Native scrolling
   means touch, trackpad, shift-wheel and keyboard all work with no code, and it
   still scrolls if the JavaScript never loads — the arrows are the only part
   that needs script.

   There is deliberately NO autoplay. Anything that moves on its own needs a
   pause control to meet WCAG 2.2.2, and a publication list is not a hero
   banner.
   ========================================================================= */
/* Arrow geometry. Three sizes, and the gutter each one needs.

   The wrapper reserves exactly the arrow plus a small gap, so an arrow sits ON
   the edge of the track and never covers a card. At 48px an overlaying arrow
   swallowed a chunk of the first title, which is what prompted this.

   Both axes reserve the INLINE gutter, even though only the horizontal variant
   puts an arrow there. That is deliberate: it keeps a horizontal and a vertical
   carousel of the same column width rendering the same card at the same size,
   which is the whole point of the two being interchangeable. Reserving it on
   the scrolling axis alone is the tidier-looking rule and it makes the two
   disagree by the width of two arrows. */
.hal-pubs {
  --hal-arrow: 38px;      /* the disc */
  --hal-arrow-ink: 17px;  /* the chevron inside it */
  --hal-arrow-gap: 10px;  /* between disc and track */
}
.hal-pubs[data-arrow-size="small"] { --hal-arrow: 30px; --hal-arrow-ink: 14px; --hal-arrow-gap: 8px; }
.hal-pubs[data-arrow-size="large"] { --hal-arrow: 48px; --hal-arrow-ink: 21px; --hal-arrow-gap: 12px; }

.hal-carousel {
  position: relative;
  padding-inline: calc(var(--hal-arrow) + var(--hal-arrow-gap));
}
/* The vertical variant needs the same room above and below, where ITS arrows
   are. The wrapper grows; the track is still exactly N cards. */
[data-carousel="vertical"] .hal-carousel {
  padding-block: calc(var(--hal-arrow) + var(--hal-arrow-gap));
}

/* Both track calcs below read --hal-gap, so EVERY layout has to state its
   track gap as that token rather than as a literal `gap`. Three of them did
   not, and a compact carousel asked for "2 cards" reserved 20px of gap where
   the track really used 8 - so a third card peeked in at the edge. There is a
   build assertion for this in tools/extract-css.mjs now.

   --hal-visible is written inline by PHP. --hal-slot is the height of ONE card:
   it is what makes "show N cards" exact, and the script overwrites it with the
   real measured height as soon as it runs. The values further down are only the
   before-script fallback.

   data-carousel lives on .hal-pubs, alongside data-layout, so every selector
   here is compound and can out-specify the ordinary layout rules. It used to
   sit on the .hal-carousel wrapper instead, which made that impossible - the
   layout rules and the carousel rules could not see each other. */
[data-carousel] .hal-list {
  scroll-behavior: smooth;
  /* Keeps the focus ring of a card's link from being clipped by the scroller. */
  scroll-padding: 3px;
  /* No scrollbar. It is a native control on a native scroller, so hiding it
     does cost an affordance - but the arrows carry that job here, they are
     always visible, and they disable at the ends, which a scrollbar cannot
     say. Scrolling itself is untouched: wheel, touch, trackpad and keyboard
     all still work. */
  scrollbar-width: none;      /* Firefox */
  -ms-overflow-style: none;   /* old Edge */
}
[data-carousel] .hal-list::-webkit-scrollbar { width: 0; height: 0; }
[data-carousel] .hal-list > * { scroll-snap-align: start; }
/* Set by the script for one frame while it measures a card's natural height.
   Nothing may constrain that height, or it would measure its own constraint. */
[data-carousel].hal-measuring .hal-list { height: auto; grid-auto-rows: auto; }
[data-carousel].hal-measuring .hal-card { height: auto; }
/* The track is focusable so it can be scrolled from the keyboard. A scrollable
   region that cannot be reached by Tab fails WCAG 2.1.1. */
[data-carousel] .hal-list:focus-visible { outline: 2px solid var(--hal-accent); outline-offset: 2px; }

[data-carousel="horizontal"] .hal-list {
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: none;
  /* Each card takes an equal share of the visible width, gaps discounted. */
  grid-auto-columns: calc(
    (100% - (var(--hal-visible, 3) - 1) * var(--hal-gap)) / var(--hal-visible, 3)
  );
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
}

[data-carousel="vertical"] .hal-list {
  display: grid;
  grid-auto-flow: row;
  grid-template-columns: minmax(0, 1fr);
  /* auto, and the CARD carries the slot height a few rules below. Sizing the
     row directly is the obvious way round, and it makes the row rather than
     the card the thing that decides a card's height - which then cannot be
     overridden per card. Letting the row follow the card keeps that door open
     and gives exactly N slots either way. */
  grid-auto-rows: auto;
  overflow-y: auto;
  overflow-x: hidden;
  scroll-snap-type: y mandatory;
  /* N cards plus the gaps between them — the height IS a number of cards. */
  height: calc(
    var(--hal-visible, 3) * var(--hal-slot) +
    (var(--hal-visible, 3) - 1) * var(--hal-gap)
  );
}
/* The Expandable template is refused a carousel upstream (see normalise() in
   class-hal-shortcode.php), which is why nothing here has to make room for a
   card that grows: inside a track, every card is exactly one slot.

   Every slot is the same height, which is what makes N exact. The script sets
   --hal-slot to the TALLEST card's natural height, so in practice nothing is
   cut off; overflow:hidden only matters before the script runs, or without it.

   The horizontal variant needs no equivalent rule: a single grid row already
   stretches every card to the tallest one, which is the same height. That is
   what keeps the two axes looking alike - the same card, same size, only the
   scroll direction differs. */
[data-carousel="vertical"] .hal-card { height: var(--hal-slot); overflow: hidden; }

/* First-paint fallback only — the script replaces these with the height it
   measures, which is the one that is actually right for this content at this
   width. They exist so the track is not zero-height before the script runs, and
   so it still works with no script at all. */
[data-carousel] { --hal-slot: 230px; }
[data-carousel][data-layout="compact"]    { --hal-slot: 190px; }
[data-carousel][data-layout="expand"]     { --hal-slot: 185px; }
[data-carousel][data-layout="list"]       { --hal-slot: 220px; }
[data-carousel][data-layout^="editorial"] { --hal-slot: 240px; }
/* A grid tile is a tall thing; three of them really is ~1900px. That is the
   honest consequence of asking for three, not something to paper over. */
[data-carousel][data-layout="grid"]       { --hal-slot: 625px; }

/* ---- arrows -------------------------------------------------------------- */
.hal-pubs .hal-carousel-btn {
  position: absolute;
  z-index: 2;
  display: grid; place-items: center;
  width: var(--hal-arrow); height: var(--hal-arrow);
  padding: 0;
  border: 1px solid var(--hal-line);
  border-radius: 50%;
  background: var(--hal-bg);
  color: var(--hal-ink-2);
  box-shadow: var(--hal-shadow-lift);
  cursor: pointer;
  transition: background .15s, border-color .15s, opacity .15s;
}
.hal-carousel-btn:hover { border-color: var(--hal-ink-3); background: var(--hal-bg-2); }
.hal-carousel-btn svg { width: var(--hal-arrow-ink); height: var(--hal-arrow-ink); }
/* Disabled rather than hidden: a control that vanishes at the ends makes the
   row jump, and its absence is harder to interpret than a dimmed button. */
.hal-carousel-btn[disabled] { opacity: .35; cursor: default; box-shadow: none; }
/* Nothing to scroll at all — then it really should not be there. */
.hal-carousel-btn[hidden] { display: none; }

/* Flush with the wrapper's outer edge, which is one gutter away from the
   track - so the disc lands on the card's edge without overlapping it.
   translate rather than a calc() offset, because the offset would have to know
   half the arrow size and there are now three of them. */
[data-carousel="horizontal"] .hal-carousel-btn { top: 50%; transform: translateY(-50%); }
[data-carousel="horizontal"] .hal-carousel-prev { left: 0; }
[data-carousel="horizontal"] .hal-carousel-next { right: 0; }

[data-carousel="vertical"] .hal-carousel-btn { left: 50%; transform: translateX(-50%); }
[data-carousel="vertical"] .hal-carousel-prev { top: 0; }
[data-carousel="vertical"] .hal-carousel-next { bottom: 0; }

/* Arrow colour, when following the section's own scheme is not what the page
   needs — pale arrows on a photo, or dark ones on a light band.

   Written as the two named palettes rather than #fff and #000, because that is
   what "white" and "black" already mean everywhere else in this component:
   a background AND its readable ink, border and hover state, all four moving
   together. A bare background colour would leave the chevron whatever the
   scheme made it, which is how you get a white arrow on a white disc. */
.hal-pubs[data-arrows="light"] .hal-carousel-btn {
  background: var(--l-bg);
  border-color: var(--l-line);
  color: var(--l-ink-2);
}
[data-arrows="light"] .hal-carousel-btn:hover {
  background: var(--l-bg-2);
  border-color: var(--l-line-strong);
}
.hal-pubs[data-arrows="dark"] .hal-carousel-btn {
  background: var(--d-bg);
  border-color: var(--d-line);
  color: var(--d-ink-2);
}
[data-arrows="dark"] .hal-carousel-btn:hover {
  background: var(--d-bg-2);
  border-color: var(--d-line-strong);
}

/* Smooth scrolling is motion. Honour the setting for the whole component. */
@media (prefers-reduced-motion: reduce) {
  [data-carousel] .hal-list { scroll-behavior: auto; }
  .hal-carousel-btn { transition: none; }
}

/* Three cards across a 470px column is three unreadable slivers, so a narrow
   container gets one card whatever the setting says.

   Full width, not width-minus-the-arrows: the arrows overlay the track edges
   here exactly as they overlay the top and bottom edges in the vertical
   variant. Reserving space on one axis and not the other was what made two
   carousels of the same width, side by side, render cards of different sizes. */
@container (max-width: 560px) {
  [data-carousel="horizontal"] .hal-list { grid-auto-columns: 100%; }

  /* Desktop gutters on a phone are a third of the column. At 390px the column
     is ~318px, and a large pair took 120px of it, leaving a 198px card. Drop to
     the small geometry whatever the setting says: the arrows are still a
     comfortable tap target and the card gets its width back.

     Set on .hal-carousel, NOT on .hal-pubs: .hal-pubs is the query container
     itself, and a container query only styles its DESCENDANTS. Written on the
     container it would match nothing (or, worse, only a nested instance). */
  .hal-carousel { --hal-arrow: 30px; --hal-arrow-ink: 14px; --hal-arrow-gap: 8px; }
}

/* ============================================================================
   RESPONSIVE HARDENING
   ========================================================================= */

/* Scholarly metadata contains long DOIs, URLs, German compounds and chemical
   names with no break opportunity. Without this they blow out the card and
   force the whole page to scroll sideways. */
.hal-title, .hal-authors, .hal-venue,
.hal-abstract, .hal-labs, .hal-kw span { overflow-wrap: anywhere; }

/* A panel must never be wider than the viewport, whatever the trigger's
   position or the theme's column width. */
.hal-ddpanel { max-width: min(320px, calc(100vw - 24px)); }

/* Once the container is narrow enough that two rows are certain, stretch the
   filter row to full width and let its controls share the space, so both rows
   end flush instead of the second stopping short. */
/* A fixed column count is a maximum, not a promise: inside a narrow column it
   has to collapse or the tracks become unreadably thin. These come after the
   [data-cols] rules on purpose - equal specificity, so source order decides. */
@container (max-width: 560px) {
  [data-layout="grid"]:not([data-carousel]) .hal-list,
  [data-layout="grid"]:not([data-carousel])[data-cols="1"] .hal-list,
  [data-layout="grid"]:not([data-carousel])[data-cols="2"] .hal-list,
  [data-layout="grid"]:not([data-carousel])[data-cols="3"] .hal-list,
  [data-layout="grid"]:not([data-carousel])[data-cols="4"] .hal-list { grid-template-columns: 1fr; }
}
@container (min-width: 560.01px) and (max-width: 860px) {
  [data-layout="grid"]:not([data-carousel])[data-cols="3"] .hal-list,
  [data-layout="grid"]:not([data-carousel])[data-cols="4"] .hal-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@container (max-width: 760px) {
  .hal-filterbar { flex: 1 1 100%; }
  .hal-filterbar > * { flex: 1 1 auto; }
  /* width:100% only inside .hal-dd, which is a block wrapper. On a pill that
     is itself a flex item it would set flex-basis to 100% and force its own
     line, which is the opposite of what we want. */
  .hal-filterbar .hal-dd .hal-pill { width: 100%; justify-content: center; }
  .hal-filterbar > .hal-pill { justify-content: center; }
  .hal-sort select { width: 100%; }
}

@media (max-width: 720px) {
  .hal-controls { padding: .55rem; }
}

@media (max-width: 480px) {

  [data-layout^="editorial"] .hal-card { gap: .7rem; padding: .8rem; }
  [data-layout="list"] .hal-card { padding: .9rem 1rem; }
  [data-layout="compact"] .hal-card { padding: .8rem .9rem; }
  [data-layout="expand"] .hal-head { padding: .85rem .9rem; gap: .8rem; }
  [data-layout="expand"] .hal-detail { padding: .8rem .9rem .9rem; }
  .hal-modal-head { padding: 1rem 1rem .85rem; }
  .hal-modal-body { padding: 1rem 1rem 1.25rem; }
  .hal-modal-head .hal-title { font-size: 1.05rem; }
}

/* Touch devices: WCAG 2.5.8 wants a 24px minimum target; 40px is comfortable.
   The desktop 34px pills are legal but fiddly with a thumb. */
@media (pointer: coarse) {
  .hal-pill,
  .hal-sort select,
  .hal-search input { height: 40px; }
  .hal-btn { padding: .5rem .8rem; }
  .hal-ddopt { padding: .55rem .5rem; }
  .hal-modal-close { width: 38px; height: 38px; }
  .hal-ddlist { max-height: 50vh; }
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ============================================================================
   WORDPRESS INTEGRATION
   ========================================================================= */

/* Uncode wraps Text Block content in <p> and sometimes <strong>. Neutralise
   inherited typography inside our container without touching the rest of the
   page. */
.hal-pubs p { margin: 0; }
.hal-pubs strong { font-weight: inherit; }
.hal-pubs ul,
.hal-pubs li { list-style: none; margin: 0; padding: 0; }
.hal-pubs a { box-shadow: none; }
.hal-pubs img { max-width: 100%; height: auto; }

.hal-pubs .hal-heading {
  margin: 0 0 1rem;
  font-size: 1.4rem;
  line-height: 1.25;
  color: var(--hal-ink);
}

/* Editor-only diagnostic, never shown to visitors. */
.hal-pubs-notice {
  padding: .8rem 1rem;
  border-left: 3px solid var(--hal-accent, #e09900);
  background: #fff8e8;
  color: #6b5300;
  font-size: .9rem;
}

/* Theme-agnostic visually-hidden helper; WP core also ships .screen-reader-text
   but themes override it inconsistently. */
.hal-pubs .screen-reader-text,
.hal-pubs .sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* Themes almost universally add margin-bottom to form controls, which breaks
   vertical centring in the filter bar. Reset ours only. Scoped with .hal-pubs
   so we out-specify the theme without !important. */
.hal-pubs input,
.hal-pubs select,
.hal-pubs button,
.hal-pubs textarea {
  margin: 0;
  font-family: inherit;
  vertical-align: middle;
}
.hal-pubs .hal-search input,
.hal-pubs .hal-pill,
.hal-pubs .hal-sort select {
  height: 34px;
  min-height: 0;
  line-height: 1;
  box-shadow: none;
}
@media (pointer: coarse) {
  .hal-pubs .hal-search input,
  .hal-pubs .hal-pill,
  .hal-pubs .hal-sort select { height: 40px; }
}
.hal-pubs .hal-controls { align-items: center; }

/* Uncode paints content links with color:inherit !important. Its own escape
   hatch is a:not(.btn) — which does NOT exclude our .hal-btn, so our
   buttons get caught and render in the theme's text colour. Specificity cannot
   beat !important, so we answer in kind, scoped strictly to our own buttons.
   This is the one place in this stylesheet where !important is warranted. */
.hal-pubs a.hal-btn,
.hal-pubs button.hal-btn { color: var(--hal-ink-2) !important; }
/* No hover colour rule here on purpose: DOI and HAL buttons change only their
   border and background on hover, exactly as originally designed. */

/* Read PDF — solid accent at rest, dropping to the outline on hover. */
.hal-pubs a.hal-btn--primary,
.hal-pubs button.hal-btn--primary { color: var(--hal-on-accent) !important; }
/* :hover and :focus-visible only - never bare :focus. A mouse click leaves
   :focus on the link, and if the colour flipped to white without the matching
   background rule the label vanished until you clicked elsewhere. */
.hal-pubs a.hal-btn--primary:hover,
.hal-pubs a.hal-btn--primary:focus-visible,
.hal-pubs button.hal-btn--primary:hover,
.hal-pubs button.hal-btn--primary:focus-visible { color: var(--hal-accent) !important; }

/* These three accent labels use the BRAND amber, at the client's request:
   Learn more at rest, Read PDF on hover, and the bottom CTA on hover. It is
   2.4:1 on white, below WCAG AA, and was chosen knowingly - the same call
   already recorded for white-on-amber in readme.txt. Small text links
   elsewhere (Clear all filters, the dropdown Clear, the typographic list's
   inline actions) keep --hal-accent-text, the AA-safe amber.
   In the dark scheme the same brand amber lands near 7:1, so nothing is
   lost there. */
.hal-pubs button.hal-btn--more { color: var(--hal-accent) !important; }
.hal-pubs button.hal-btn--more:hover,
.hal-pubs button.hal-btn--more:focus-visible { color: var(--hal-on-accent) !important; }

/* The bottom call to action. It is not a .hal-btn, so the rules above miss it,
   but Uncode's a:not(.btn){color:inherit!important} catches it just the same
   and would repaint the label in the theme's ink on an accent fill. */
.hal-pubs .hal-cta a { color: var(--hal-on-accent) !important; }
.hal-pubs .hal-cta a:hover,
.hal-pubs .hal-cta a:focus-visible { color: var(--hal-accent) !important; }

/* The typographic list renders actions as inline text links, not filled buttons. */
.hal-pubs[data-layout="list"] a.hal-btn,
.hal-pubs[data-layout="list"] button.hal-btn { color: var(--hal-accent-text) !important; }

/* Uncode removes the focus ring site-wide with
     body:not(.uncode-accessible) *:focus { outline: 0 }
   which scores (0,2,1) and beats a plain .hal-btn:focus-visible (0,2,0). The
   result is that keyboard focus moves but is invisible, which is
   indistinguishable from Tab being broken. Restore it for our controls only.
   !important because the theme rule wins on specificity either way. */
.hal-pubs a:focus-visible,
.hal-pubs button:focus-visible,
.hal-pubs input:focus-visible,
.hal-pubs select:focus-visible,
.hal-pubs textarea:focus-visible,
.hal-pubs [tabindex]:focus-visible {
  outline: 2px solid var(--hal-accent) !important;
  outline-offset: 2px !important;
}
/* The search box and in-panel filter use a ring instead of an outline, so they
   keep their own treatment but must still show something. */
.hal-pubs .hal-search input:focus,
.hal-pubs .hal-ddsearch input:focus {
  outline: 2px solid var(--hal-accent) !important;
  outline-offset: 1px !important;
}
/* Checkboxes inside the dropdowns rely on the browser default, which the theme
   also suppresses. */
.hal-pubs .hal-ddopt input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--hal-accent) !important;
  outline-offset: 1px !important;
}

/* The no-JS open-access checkbox that pairs with the visible pill. */
.hal-pubs .hal-oa-input { position: absolute; opacity: 0; pointer-events: none; }
