-
- .csa-carousel itself is a flex row of exactly those three children
- (arrow, viewport, arrow) — the arrows take up their own real space
- in that row rather than sitting absolutely positioned on top of the
- viewport, so they can never overlap the cards no matter how wide the
- arrows or how narrow the cards are. .csa-carousel-viewport is the
- flex-shrink target (flex:1; min-width:0 — the min-width is what lets
- a flex item shrink below its content's natural width, which a
- viewport that needs to clip a wider track always wants).
+ .csa-carousel is the outer wrapper — a plain flex column stacking
+ .csa-carousel-row above .csa-carousel-dots, so copying the one
+ .csa-carousel block always brings the dots with it. (An earlier
+ version of this file had the dots as a sibling *after* .csa-carousel
+ instead of a child — easy to miss when copying just the block you
+ can see is "the carousel," which is exactly what happened on a real
+ page. Nesting it removes that trap entirely.) .csa-carousel-row is
+ the flex row of exactly those three children (arrow, viewport,
+ arrow) — the arrows take up their own real space in that row rather
+ than sitting absolutely positioned on top of the viewport, so they
+ can never overlap the cards no matter how wide the arrows or how
+ narrow the cards are. .csa-carousel-viewport is the flex-shrink
+ target (flex:1; min-width:0 — the min-width is what lets a flex item
+ shrink below its content's natural width, which a viewport that
+ needs to clip a wider track always wants).
.csa-dragging is a hook this CSS already reads (switches the track's
cursor to grabbing) — toggle it while a drag is in progress, if your
implementation supports dragging.
- .csa-carousel-dots (optional) is a row of progress dots below the
- viewport — .csa-carousel-dot is the base state, .csa-carousel-dot-active
- the current page. Which dot is active is real app state (it tracks
- scroll/drag position, same as the drag/arrow behavior above) — this
- library only styles the two states, your implementation decides how
- many dots to render and which one gets the active class, and updates
- it on scroll/drag/arrow-click.
+ .csa-carousel-dots (optional — omit the element entirely if you
+ don't need it, .csa-carousel's own gap only ever applies between
+ children that exist) is a row of progress dots below
+ .csa-carousel-row — .csa-carousel-dot is the base state,
+ .csa-carousel-dot-active the current page. Which dot is active is
+ real app state (it tracks scroll/drag position, same as the
+ drag/arrow behavior above) — this library only styles the two
+ states, your implementation decides how many dots to render and
+ which one gets the active class, and updates it on
+ scroll/drag/arrow-click.
No edge fade: cards are narrow enough relative to the viewport that
the next card visibly peeks in from the side, which reads as "more
@@ -62,7 +77,8 @@
purely so the page is interactive to browse — see src/docs/index.html.
That script is not part of the library and isn't bundled by build.mjs.)
========================================================= */
-.csa-carousel { display: flex; align-items: center; gap: 16px; }
+.csa-carousel { display: flex; flex-direction: column; gap: 24px; }
+.csa-carousel-row { display: flex; align-items: center; gap: 16px; }
.csa-carousel-viewport { flex: 1; min-width: 0; overflow: hidden; }
.csa-carousel-track { display: flex; gap: 20px; cursor: grab; will-change: transform; touch-action: pan-y; }
@@ -124,7 +140,7 @@
resets font/color on form controls, not appearance/border chrome
(font: inherit below is now redundant with that global reset, kept
anyway so this rule stays self-contained if it's ever copied out). */
-.csa-carousel-dots { display: flex; justify-content: center; align-items: center; gap: 7px; margin-top: 24px; }
+.csa-carousel-dots { display: flex; justify-content: center; align-items: center; gap: 7px; }
.csa-carousel-dot {
appearance: none; border: none; font: inherit;
width: 8px; height: 8px; padding: 0; border-radius: 99px;
@@ -134,7 +150,7 @@
@media (max-width: 900px) { .csa-carousel-card { flex: 0 0 70%; max-width: 328px; } }
@media (max-width: 560px) {
- .csa-carousel { gap: 10px; }
+ .csa-carousel-row { gap: 10px; }
.csa-carousel-arrow { width: 40px; height: 40px; }
.csa-carousel-arrow svg { width: 18px; height: 18px; }
.csa-carousel-card { flex: 0 0 88%; }
diff --git a/src/components/hero.css b/src/components/hero.css
index ec0e2b3..9178a81 100644
--- a/src/components/hero.css
+++ b/src/components/hero.css
@@ -275,6 +275,46 @@
.csa-hero-graphic-content { max-width: none; }
}
+/* Hero-split-graphic — content floats directly on the photo (no glass
+ panel, same as .csa-hero-photo alone), split into two columns by
+ .csa-wrap itself: text/CTA on the left, a single graphic (a logo/
+ wordmark, not an illustration meant to overlap the frame the way
+ .csa-hero-glass-graphic's does) vertically centered on the right.
+ Promoted from the STAR Program's own "Learn about STAR" hero pattern
+ — reused as-is across 4 of its pages (Learn about STAR, Learn about
+ STAR for AI, STAR Registry, Get Listed) before landing here, so this
+ is a proven shape, not a speculative one.
+
+ Markup:
+
+
+
...badges/kicker/h1/sub/cta...
+

+
+
+
+ 1.2fr for the text column (not a plain 1fr/1fr split) gives copy the
+ larger share — a headline + paragraph + CTA row needs more room to
+ breathe than a single static graphic; the graphic column is `auto`,
+ sized to .csa-hero-split-img's own width:100%/max-width below (286px
+ — the STAR source's real value, not a guess) rather than stretched.
+ Below 1050px the graphic drops under the text instead of squeezing
+ beside it — the same breakpoint .csa-hero-glass-graphic already uses
+ for its own graphic column, kept consistent across both hero
+ patterns; the image also shrinks to a smaller 200px cap there,
+ matching the source's own mobile behavior. */
+.csa-hero-split-graphic .csa-wrap {
+ display: grid;
+ grid-template-columns: 1.2fr auto;
+ gap: 48px;
+ align-items: center;
+}
+.csa-hero-split-img { display: block; justify-self: end; width: 100%; max-width: 286px; }
+@media (max-width: 1050px) {
+ .csa-hero-split-graphic .csa-wrap { grid-template-columns: 1fr; }
+ .csa-hero-split-img { justify-self: center; max-width: 200px; margin-top: 8px; }
+}
+
/* Closing CTA band */
.csa-cta {
position: relative; overflow: hidden;
diff --git a/src/components/pagination.css b/src/components/pagination.css
index 7136935..a8cd4a3 100644
--- a/src/components/pagination.css
+++ b/src/components/pagination.css
@@ -46,24 +46,24 @@
color) keeps the base n500. */
.csa-pagination-btn[aria-current="page"] { background: var(--b500); border-color: var(--b500); color: #fff; }
.csa-pagination-btn:disabled { color: var(--n200); border-color: var(--n50); cursor: not-allowed; }
-/* Prev's disabled state (page 1) borrows .csa-btn-disabled's exact
- colors (buttons.css) rather than this component's own plain-
- disabled treatment above — same fill-not-just-faded-text look as
- every other disabled control in this library, and higher
+/* Prev/Next's disabled state (first/last page) borrows .csa-btn-
+ disabled's exact colors (buttons.css) rather than this component's
+ own plain-disabled treatment above — same fill-not-just-faded-text
+ look as every other disabled control in this library, and higher
specificity (two classes) than the plain :disabled rule above wins
it automatically. */
-.csa-pagination-prev:disabled { background: var(--n50); color: var(--n300); border-color: transparent; }
+.csa-pagination-prev:disabled, .csa-pagination-next:disabled { background: var(--n50); color: var(--n300); border-color: transparent; }
/* ---- Surface-aware — dark ---- */
[data-surface="dark"] .csa-pagination-btn { border-color: var(--b600); color: var(--n75); background: transparent; }
[data-surface="dark"] .csa-pagination-btn:hover { border-color: var(--b500); }
[data-surface="dark"] .csa-pagination-btn[aria-current="page"] { background: var(--b300); border-color: var(--b300); color: #fff; }
[data-surface="dark"] .csa-pagination-btn:disabled { color: var(--b600); border-color: var(--b700); }
-[data-surface="dark"] .csa-pagination-prev:disabled { background: var(--b800); color: var(--b500); border-color: transparent; }
+[data-surface="dark"] .csa-pagination-prev:disabled, [data-surface="dark"] .csa-pagination-next:disabled { background: var(--b800); color: var(--b500); border-color: transparent; }
/* ---- Surface-aware — black ---- */
[data-surface="black"] .csa-pagination-btn { border-color: var(--n700); color: var(--n75); background: transparent; }
[data-surface="black"] .csa-pagination-btn:hover { border-color: var(--n600); }
[data-surface="black"] .csa-pagination-btn[aria-current="page"] { background: var(--b300); border-color: var(--b300); color: #fff; }
[data-surface="black"] .csa-pagination-btn:disabled { color: var(--n600); border-color: var(--n800); }
-[data-surface="black"] .csa-pagination-prev:disabled { background: var(--n800); color: var(--n500); border-color: transparent; }
+[data-surface="black"] .csa-pagination-prev:disabled, [data-surface="black"] .csa-pagination-next:disabled { background: var(--n800); color: var(--n500); border-color: transparent; }
diff --git a/src/components/tabs.css b/src/components/tabs.css
index 000b1ec..c53eaa2 100644
--- a/src/components/tabs.css
+++ b/src/components/tabs.css
@@ -54,6 +54,7 @@
[data-surface="black"] .csa-tablist { border-bottom-color: var(--n700); }
.csa-tab {
+ position: relative; z-index: 0;
appearance: none; background: transparent; border: none; cursor: pointer;
font-family: inherit; font-size: 14.5px; font-weight: 600;
color: var(--n300); padding: 12px 16px 14px;
@@ -63,9 +64,34 @@
[data-surface="dark"] .csa-tab { color: var(--n100); }
[data-surface="black"] .csa-tab { color: var(--n300); }
+/* Hover fill is a separate ::before, not the tab's own background —
+ inset stops short of the tab's bottom edge (where .csa-tablist's
+ own border-bottom, or the active tab's indicator, lives) so the
+ rounded highlight box floats just above that line instead of
+ touching it. position:relative + z-index:0 on .csa-tab (above)
+ gives this its own local stacking context, so z-index:-1 here only
+ drops it behind the tab's own label text, not behind the page. */
+.csa-tab::before {
+ content: ''; position: absolute; z-index: -1;
+ inset: 4px 2px 8px; border-radius: 8px;
+ background: transparent; transition: background-color .18s;
+}
+/* n50, not n25 — n25 would be invisible against an n25 page (the same
+ escalation problem tags.css/toggles.css already document), and n50
+ still reads clearly on plain white too. At 50% opacity (not flat)
+ so it softens rather than fully overriding whatever's underneath —
+ rgba, since a CSS variable can't be given partial opacity directly;
+ #DDE6EF (--n50) at 50%. */
.csa-tab:hover { color: var(--b500); }
+.csa-tab:hover::before { background: rgba(221,230,239,.5); }
+/* One ramp step lighter than each surface's own page background
+ (surfaces.css: b700/black) — the same "lift, don't sink" logic
+ filters.css's row hover uses; a darker fill here would read as a
+ hole in the tab row instead of a highlight. */
[data-surface="dark"] .csa-tab:hover { color: #fff; }
+[data-surface="dark"] .csa-tab:hover::before { background: var(--b600); }
[data-surface="black"] .csa-tab:hover { color: #fff; }
+[data-surface="black"] .csa-tab:hover::before { background: var(--n700); }
.csa-tab-active,
.csa-tab-active:hover {
diff --git a/src/components/tags.css b/src/components/tags.css
index 1147a0d..2312968 100644
--- a/src/components/tags.css
+++ b/src/components/tags.css
@@ -71,7 +71,19 @@
.csa-tag-filter.csa-tag-blue[aria-pressed="true"] { background: var(--b500); color: #fff; }
.csa-tag-filter.csa-tag-orange[aria-pressed="true"] { background: var(--o700); color: #fff; }
-/* ---- Neutral (n25) background — one ramp step up so tags stay visible against their own n25 host ---- */
+/* ---- Neutral (n25) background — one ramp step up so tags stay visible against their own n25 host ----
+ Pairing rule, so the wrong variant never gets picked by accident:
+ .csa-bg-n25 only belongs on an ancestor whose OWN rendered background
+ really is n25 (the page, or a plain n25 panel) — never combine it
+ with something that then forces the immediate container to a
+ DIFFERENT color (e.g. a wrapper carrying .csa-bg-n25 purely to turn a
+ nested .csa-card white). That mismatch — tags escalated for n25
+ sitting on a card that's actually white — is exactly the bug this
+ comment exists to prevent; it was shipped once in this project's own
+ docs showcase before being caught and fixed. If a card needs to be
+ white specifically, style that card directly (background/border
+ inline, or its own class) rather than reaching for .csa-bg-n25 on an
+ ancestor. */
.csa-bg-n25 .csa-tag-neutral { background: var(--n50); }
.csa-bg-n25 .csa-tag-blue { background: var(--b50); }
.csa-bg-n25 .csa-tag-orange { background: var(--o50); }
@@ -127,7 +139,7 @@
.csa-icon-glyph primitive in base/icon-glyph.css — this just sizes
it for the tag context; its 14px default already matches what tags
need, so no override is required here. That's also why
- Profile-white.svg (mentioned as already existing for the pressed
+ profile-white.svg (mentioned as already existing for the pressed
state) isn't wired in separately: masking already renders the glyph
in solid white the instant the pressed rules above set color:#fff,
so a second file would only reproduce what currentColor already
diff --git a/src/docs/assets/hero-bg-star.jpg b/src/docs/assets/hero-bg-star.jpg
new file mode 100644
index 0000000..1729e18
Binary files /dev/null and b/src/docs/assets/hero-bg-star.jpg differ
diff --git a/src/docs/index.html b/src/docs/index.html
index 890859d..006d307 100644
--- a/src/docs/index.html
+++ b/src/docs/index.html
@@ -956,6 +956,13 @@
Card heading goes here