diff --git a/web/stack.css b/web/stack.css
index 268520a..cbf32a3 100644
--- a/web/stack.css
+++ b/web/stack.css
@@ -28,6 +28,14 @@
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
--radius: 10px;
--wide: 64rem;
+
+ /* The three measurements the shared family blocks at the end of this file
+ * read. They are the only thing about those blocks that is allowed to differ
+ * between the three sample repositories, because the three pages are built
+ * around containers of different widths. */
+ --family-width: var(--wide); /* what .masthead and main centre in */
+ --family-gutter: 0rem; /* those have no side padding of their own */
+ --family-bleed: 1.25rem; /* has this much, and the bar undoes it */
}
@media (prefers-color-scheme: dark) {
@@ -418,7 +426,7 @@ mark {
.empty { margin: 2rem 0; color: var(--ink-soft); }
-.how, .packages, .ladder {
+.how, .packages {
margin: 3rem 0 0;
padding-top: 1.4rem;
border-top: 1px solid var(--line);
@@ -426,21 +434,21 @@ mark {
scroll-margin-top: 15rem;
}
-.how h2, .packages h2, .ladder h2 {
+.how h2, .packages h2 {
margin: 0 0 .7rem;
font-size: 1.15rem;
letter-spacing: -.01em;
}
-.how ol, .steps {
+.how ol {
max-width: 46rem;
margin: 0;
padding-left: 1.2rem;
color: var(--ink-soft);
font-size: .92rem;
}
-.how li, .steps li { margin: .45rem 0; }
-.how b, .steps b { color: var(--ink); }
+.how li { margin: .45rem 0; }
+.how b { color: var(--ink); }
#pkgcards {
display: grid;
@@ -502,3 +510,181 @@ footer p { margin: .4rem 0; }
.facts > div { grid-template-columns: 1fr; gap: 0; }
.facts dt { font-size: .74rem; text-transform: uppercase; }
}
+
+/* ------------------------------------------------------------ family nav */
+
+/* family-nav:start
+ *
+ * The bar that ties the three sample pages together, and the card strip at the
+ * end of the page that says why you would go next door. Both are IDENTICAL in
+ * abap2UI5/samples, abap2UI5/samples-controls and abap2UI5/samples-stack -
+ * change them in all three repositories or in none. `npm run check:family-nav`
+ * fails when a copy drifts.
+ *
+ * They read three tokens each page sets for itself in :root and nothing else
+ * of their own: --family-width (the container the rest of the page centres
+ * in), --family-gutter (that container's own side padding) and --family-bleed
+ * (the side padding on , which the bar breaks out of to reach the edge).
+ * Every colour comes from the palette above, so light and dark come free.
+ *
+ * The bar is deliberately NOT sticky: two of the three pages already stick
+ * their filter panel to the top, and a second sticky strip above it would eat
+ * a phone screen twice over. */
+
+.family {
+ margin: 0 calc(var(--family-bleed) * -1);
+ padding: 0 var(--family-bleed);
+ background: var(--panel);
+ border-bottom: 1px solid var(--line);
+}
+
+.family-inner {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: .1rem;
+ max-width: var(--family-width);
+ margin: 0 auto;
+ padding: .35rem var(--family-gutter);
+}
+
+.family-brand {
+ margin-right: .7rem;
+ font: 700 .78rem/1 var(--mono);
+ letter-spacing: .04em;
+ color: var(--ink);
+ text-decoration: none;
+ white-space: nowrap;
+}
+.family-brand:hover { color: var(--accent); }
+
+/* The verb is what a reader recognises - "samples-controls" tells a newcomer
+ * nothing, "Controls / every UI5 control, searchable" tells them everything -
+ * so the repository name is in the tooltip and the footer, not here. */
+.family-page {
+ display: inline-flex;
+ align-items: baseline;
+ gap: .45em;
+ padding: .3em .65em;
+ border: 1px solid transparent;
+ border-radius: 99px;
+ font-size: .85rem;
+ text-decoration: none;
+}
+.family-page b { font-weight: 600; color: var(--ink); }
+.family-page span { font-size: .8rem; color: var(--ink-faint); }
+.family-page:hover { background: var(--line-soft); }
+
+/* The one thing this bar has to say without spending a word on it: which of
+ * the three you are reading. Drawn off aria-current, so the markup is the
+ * same file on all three pages and only that attribute moves.
+ *
+ * The current page is coloured with --accent and not --accent-ink: the two
+ * repositories next door use --accent-ink for text ON the accent fill, where
+ * it is white, and white on --accent-soft is nothing at all. */
+.family-page[aria-current="page"] {
+ background: var(--accent-soft);
+ border-color: var(--accent);
+}
+.family-page[aria-current="page"] b { color: var(--accent); }
+.family-page[aria-current="page"] span { color: var(--ink-soft); }
+
+.family-spacer { flex: 1 1 auto; }
+
+/* The playground and the documentation are tools, not sibling pages: they sit
+ * apart from the three and carry the outward arrow. */
+.family-tool {
+ padding: .3em .5em;
+ font-size: .8rem;
+ color: var(--ink-soft);
+ text-decoration: none;
+ white-space: nowrap;
+}
+.family-tool:hover { color: var(--accent); }
+
+.family a:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 1px;
+ border-radius: 99px;
+}
+
+/* On a phone the three verbs and the two tools still fit once the subtitles
+ * go. Dropping the bar entirely would be dropping the way out. */
+@media (max-width: 47rem) {
+ .family-page span { display: none; }
+ .family-brand { margin-right: .35rem; }
+}
+
+/* --------------------------------------------------------- three pages */
+
+/* The bar at the top answers "where am I". This answers "why would I go
+ * anywhere else", and it sits at the end because that is where a reader who
+ * is done with this page arrives. */
+
+.three {
+ max-width: var(--family-width);
+ margin: 3rem auto 0;
+ /* A rule above it, because on one of the three pages this lands directly
+ * under another prose section that has one, and without it the strip reads
+ * as that section's tail rather than as its own thing. */
+ padding: 1.4rem var(--family-gutter) 0;
+ border-top: 1px solid var(--line);
+}
+.three > h2 { margin: 0 0 .3rem; font-size: 1.15rem; }
+.three-lede {
+ max-width: 46rem;
+ margin: 0 0 1rem;
+ font-size: .9rem;
+ color: var(--ink-soft);
+}
+
+.three-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
+ gap: .7rem;
+}
+
+.three-card {
+ display: block;
+ padding: .85rem .95rem;
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ color: inherit;
+ text-decoration: none;
+}
+.three-card:hover { border-color: var(--accent); }
+.three-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
+
+.three-verb {
+ display: block;
+ margin-bottom: .15rem;
+ font: 600 .7rem/1 var(--mono);
+ letter-spacing: .08em;
+ text-transform: uppercase;
+ color: var(--accent);
+}
+.three-repo { display: block; font-size: .95rem; font-weight: 600; color: var(--ink); }
+.three-q {
+ display: block;
+ margin: .3rem 0 .2rem;
+ font-size: .88rem;
+ font-style: italic;
+ color: var(--ink);
+}
+.three-what { display: block; font-size: .85rem; color: var(--ink-soft); }
+
+/* Same trick as the bar, same reason. */
+.three-here { display: none; }
+.three-card[aria-current="page"] {
+ background: var(--accent-soft);
+ border-color: var(--accent);
+}
+.three-card[aria-current="page"] .three-here {
+ display: block;
+ margin-top: .5rem;
+ font: 600 .74rem/1 var(--mono);
+ color: var(--accent);
+}
+
+/* family-nav:end */