@rxova/brand 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxova/brand",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Design tokens, Starlight theme and shared site chrome for rxova.org",
6
6
  "keywords": [
package/src/astro.css CHANGED
@@ -13,6 +13,10 @@
13
13
  */
14
14
 
15
15
  @import './tokens.css';
16
+ /* The shared site footer, which /blog, /updates and the landing all render. It
17
+ is the one piece of chrome common to these surfaces and the Starlight ones,
18
+ so its rules sit in their own file and both entry points pull them in. */
19
+ @import './footer.css';
16
20
 
17
21
  :root {
18
22
  --bg: var(--rx-bg);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  /**
3
- * The four-column rxova footer, shared by all four surfaces.
3
+ * The rxova footer, shared by every surface.
4
4
  *
5
5
  * Before this, the landing had a single dot-separated link row and the three
6
6
  * docs sites had a bare copyright line (journey went further and hid its footer
@@ -8,6 +8,14 @@
8
8
  * make one origin feel like one site — so it carries the full project list on
9
9
  * every page.
10
10
  *
11
+ * Shape: a brand block that says what rxova is, the link columns, then a bottom
12
+ * bar with the copyright and the legal links. The legal links are down there
13
+ * rather than in a column because that is where a reader looks for them, and it
14
+ * keeps the columns to things someone might actually want to click.
15
+ *
16
+ * Styles are in `../footer.css`, imported by both `astro.css` and
17
+ * `starlight.css` — see that file for why they are not scoped here.
18
+ *
11
19
  * All cross-project hrefs are absolute; see sites.ts for why.
12
20
  */
13
21
  import { PROJECTS, RXOVA_ORIGIN, getProject, siteUrl, type ProjectId } from '../sites.ts'
@@ -25,72 +33,102 @@ const year = new Date().getFullYear()
25
33
  ---
26
34
 
27
35
  <footer class="rx-footer">
28
- <div class="rx-footer__columns">
29
- {
30
- docs && docs.length > 0 && (
31
- <div>
32
- <h2 class="rx-footer__title">Docs</h2>
33
- <ul class="rx-footer__list">
34
- {docs.map((link) => (
36
+ <div class="rx-footer__top">
37
+ <div class="rx-footer__brand">
38
+ <a class="rx-footer__mark" href={RXOVA_ORIGIN}>
39
+ <img src={siteUrl('/rxova-logo-256.png')} alt="" width="28" height="28" loading="lazy" />
40
+ <span>rxova</span>
41
+ </a>
42
+ <p class="rx-footer__blurb">
43
+ Open-source React libraries, documented in one place. MIT licensed, built in the open.
44
+ </p>
45
+ </div>
46
+
47
+ <div class="rx-footer__columns">
48
+ {
49
+ docs && docs.length > 0 && (
50
+ <div>
51
+ <h2 class="rx-footer__title">Docs</h2>
52
+ <ul class="rx-footer__list">
53
+ {docs.map((link) => (
54
+ <li>
55
+ <a href={link.href}>{link.label}</a>
56
+ </li>
57
+ ))}
58
+ </ul>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ <div>
64
+ <h2 class="rx-footer__title">Projects</h2>
65
+ <ul class="rx-footer__list">
66
+ {
67
+ PROJECTS.map((p) => (
35
68
  <li>
36
- <a href={link.href}>{link.label}</a>
69
+ <a href={`${RXOVA_ORIGIN}${p.mount}`}>{p.label}</a>
37
70
  </li>
38
- ))}
39
- </ul>
40
- </div>
41
- )
42
- }
71
+ ))
72
+ }
73
+ </ul>
74
+ </div>
43
75
 
44
- <div>
45
- <h2 class="rx-footer__title">Projects</h2>
46
- <ul class="rx-footer__list">
47
- <li><a href={RXOVA_ORIGIN}>rxova.org</a></li>
48
- {
49
- PROJECTS.map((p) => (
50
- <li>
51
- <a href={`${RXOVA_ORIGIN}${p.mount}`}>{p.label}</a>
52
- </li>
53
- ))
54
- }
55
- </ul>
56
- </div>
76
+ {
77
+ self && (
78
+ <div>
79
+ <h2 class="rx-footer__title">Community</h2>
80
+ <ul class="rx-footer__list">
81
+ <li>
82
+ <a href={self.repo}>GitHub</a>
83
+ </li>
84
+ <li>
85
+ <a href={`${self.repo}/issues`}>Issues</a>
86
+ </li>
87
+ <li>
88
+ <a href={`${self.repo}/discussions`}>Discussions</a>
89
+ </li>
90
+ <li>
91
+ <a href={self.npm}>npm</a>
92
+ </li>
93
+ <li>
94
+ <a href={`${self.repo}/blob/main/CONTRIBUTING.md`}>Contributing</a>
95
+ </li>
96
+ </ul>
97
+ </div>
98
+ )
99
+ }
57
100
 
58
- {
59
- self && (
60
- <div>
61
- <h2 class="rx-footer__title">Community</h2>
62
- <ul class="rx-footer__list">
63
- <li>
64
- <a href={self.repo}>GitHub</a>
65
- </li>
66
- <li>
67
- <a href={`${self.repo}/issues`}>Issues</a>
68
- </li>
69
- <li>
70
- <a href={`${self.repo}/discussions`}>Discussions</a>
71
- </li>
72
- <li>
73
- <a href={self.npm}>npm</a>
74
- </li>
75
- <li>
76
- <a href={`${self.repo}/blob/main/CONTRIBUTING.md`}>Contributing</a>
77
- </li>
78
- </ul>
79
- </div>
80
- )
81
- }
101
+ {
102
+ /* Blog and Updates are here on every surface, docs sites included: they
103
+ were reachable only from the umbrella landing, which meant a reader
104
+ who arrived on a docs page from a search result never learned the
105
+ rest of the site existed. */
106
+ }
107
+ <div>
108
+ <h2 class="rx-footer__title">Site</h2>
109
+ <ul class="rx-footer__list">
110
+ <li><a href={RXOVA_ORIGIN}>Home</a></li>
111
+ <li><a href={siteUrl('/blog')}>Blog</a></li>
112
+ <li><a href={siteUrl('/updates')}>Updates</a></li>
113
+ </ul>
114
+ </div>
82
115
 
83
- <div>
84
- <h2 class="rx-footer__title">rxova</h2>
85
- <ul class="rx-footer__list">
86
- <li><a href={RXOVA_ORIGIN}>Home</a></li>
87
- <li><a href="https://github.com/rxova">GitHub org</a></li>
88
- <li><a href={siteUrl('/privacy')}>Privacy</a></li>
89
- <li><a href={siteUrl('/terms')}>Terms</a></li>
90
- <li><a href="mailto:rxova@proton.me">rxova@proton.me</a></li>
91
- </ul>
116
+ <div>
117
+ <h2 class="rx-footer__title">Connect</h2>
118
+ <ul class="rx-footer__list">
119
+ <li><a href="https://github.com/rxova">GitHub</a></li>
120
+ <li><a href="https://www.npmjs.com/org/rxova">npm</a></li>
121
+ <li><a href="mailto:rxova@proton.me">rxova@proton.me</a></li>
122
+ </ul>
123
+ </div>
92
124
  </div>
93
125
  </div>
94
126
 
95
- <p class="rx-footer__legal">© {year} rxova · MIT licensed</p>
127
+ <div class="rx-footer__legal">
128
+ <p>© {year} rxova · MIT licensed</p>
129
+ <div class="rx-footer__legal-links">
130
+ <a href={siteUrl('/privacy')}>Privacy</a>
131
+ <a href={siteUrl('/terms')}>Terms</a>
132
+ </div>
133
+ </div>
96
134
  </footer>
@@ -14,6 +14,7 @@ import '../astro.css'
14
14
 
15
15
  import { RXOVA_ORIGIN, siteUrl } from '../sites.ts'
16
16
 
17
+ import SiteFooter from './SiteFooter.astro'
17
18
  import ThemeScript from './ThemeScript.astro'
18
19
  import ThemeToggle from './ThemeToggle.astro'
19
20
 
@@ -27,7 +28,6 @@ interface Props {
27
28
 
28
29
  const { title, description, path, ogType = 'website' } = Astro.props
29
30
  const canonical = `${RXOVA_ORIGIN}${path}`
30
- const year = new Date().getFullYear()
31
31
 
32
32
  /**
33
33
  * The menu, in one fixed order for every surface.
@@ -72,9 +72,13 @@ const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
72
72
  <ThemeScript />
73
73
  </head>
74
74
  <body>
75
- <ThemeToggle />
76
- <main>
77
- <header class="site">
75
+ {
76
+ /* Outside <main> so it can span the viewport while its contents stay on
77
+ the same measure as the page. Sticky rather than fixed: it still takes
78
+ part in layout, so nothing needs a compensating top margin. */
79
+ }
80
+ <header class="site">
81
+ <div class="site__inner">
78
82
  <a class="brand" href={siteUrl('/')}>
79
83
  <img src={siteUrl('/rxova-logo-256.png')} alt="" width="28" height="28" />
80
84
  <span>rxova</span>
@@ -98,35 +102,60 @@ const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
98
102
  )
99
103
  }
100
104
  </nav>
101
- </header>
105
+ {/* In the header, not floating over it — see ThemeToggle's `floating`. */}
106
+ <ThemeToggle floating={false} />
107
+ </div>
108
+ </header>
102
109
 
110
+ <main>
103
111
  <slot />
104
-
105
- <footer class="foot">
106
- <span>© {year} rxova</span>
107
- <span class="dot" aria-hidden="true">·</span>
108
- <a href={siteUrl('/')}>Projects</a>
109
- <span class="dot" aria-hidden="true">·</span>
110
- <a href={siteUrl('/blog')}>Blog</a>
111
- <span class="dot" aria-hidden="true">·</span>
112
- <a href={siteUrl('/updates')}>Updates</a>
113
- <span class="dot" aria-hidden="true">·</span>
114
- <a href={siteUrl('/privacy')}>Privacy</a>
115
- <span class="dot" aria-hidden="true">·</span>
116
- <a href={siteUrl('/terms')}>Terms</a>
117
- </footer>
118
112
  </main>
119
113
 
114
+ <div class="foot">
115
+ <SiteFooter />
116
+ </div>
117
+
120
118
  <style>
119
+ /* Sticky, translucent, full-bleed. It was a plain in-flow row with the
120
+ theme button floating over its right end, which on a phone put the
121
+ button on top of the nav links and left the header behind as soon as
122
+ you scrolled — a long post gave you no way back without scrolling to
123
+ the top. */
121
124
  .site {
125
+ position: sticky;
126
+ top: 0;
127
+ z-index: 10;
128
+ border-bottom: 1px solid var(--rule);
129
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
130
+ }
131
+
132
+ /* The blur is what keeps text legible as it passes under the bar. Where
133
+ it is unsupported the background falls back to nearly opaque, which is
134
+ why the mix above is 88% and not 60%. */
135
+ @supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
136
+ .site {
137
+ background: color-mix(in srgb, var(--bg) 72%, transparent);
138
+ -webkit-backdrop-filter: blur(10px);
139
+ backdrop-filter: blur(10px);
140
+ }
141
+ }
142
+
143
+ .site__inner {
144
+ max-width: var(--max);
145
+ margin: 0 auto;
146
+ padding: 0.75rem 1.5rem;
122
147
  display: flex;
123
148
  align-items: center;
124
- justify-content: space-between;
125
149
  gap: 1rem;
126
- flex-wrap: wrap;
127
- padding-bottom: 1.25rem;
128
- margin-bottom: clamp(2rem, 6vh, 3rem);
129
- border-bottom: 1px solid var(--rule);
150
+ /* No wrapping: a two-line header would eat a phone's viewport, and the
151
+ row is sized to fit at the narrowest widths we support. */
152
+ min-height: 3.5rem;
153
+ }
154
+
155
+ /* The nav takes the space between the brand and the toggle, and pushes
156
+ itself to the right of it. */
157
+ .site nav {
158
+ margin-left: auto;
130
159
  }
131
160
  .brand {
132
161
  display: inline-flex;
@@ -149,6 +178,7 @@ const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
149
178
  color: var(--muted);
150
179
  text-decoration: none;
151
180
  font-size: 0.95rem;
181
+ white-space: nowrap;
152
182
  }
153
183
  .site nav a:hover {
154
184
  color: var(--fg);
@@ -159,32 +189,39 @@ const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
159
189
  text-underline-offset: 5px;
160
190
  }
161
191
 
192
+ /* Brand mark, three links and the toggle at 390px: tight, but it fits on
193
+ one row, which is worth more here than the extra breathing room. */
194
+ @media (max-width: 34rem) {
195
+ .site__inner {
196
+ gap: 0.75rem;
197
+ padding-inline: 1rem;
198
+ min-height: 3.25rem;
199
+ }
200
+ .site nav {
201
+ gap: 0.9rem;
202
+ }
203
+ .site nav a {
204
+ font-size: 0.875rem;
205
+ }
206
+ .brand span {
207
+ /* The logo carries the identity in the space left; the word does not
208
+ fit beside three links on the narrowest phones. */
209
+ display: none;
210
+ }
211
+ }
212
+
162
213
  main {
163
214
  max-width: var(--max);
164
215
  margin: 0 auto;
165
216
  padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
166
217
  }
167
218
 
219
+ /* The footer's own rules are unscoped (footer.css, shared with the docs
220
+ sites); this only puts it on the page's measure. */
168
221
  .foot {
169
- margin-top: clamp(3rem, 8vh, 5rem);
170
- padding-top: 1.75rem;
171
- border-top: 1px solid var(--rule);
172
- color: var(--faint);
173
- font-size: 0.9rem;
174
- display: flex;
175
- align-items: center;
176
- gap: 0.6rem;
177
- flex-wrap: wrap;
178
- }
179
- .foot a {
180
- color: var(--muted);
181
- text-decoration: none;
182
- }
183
- .foot a:hover {
184
- color: var(--fg);
185
- }
186
- .dot {
187
- color: var(--rule);
222
+ max-width: var(--max);
223
+ margin: 0 auto;
224
+ padding: 0 1.5rem;
188
225
  }
189
226
  </style>
190
227
 
@@ -193,6 +230,12 @@ const inside = (p = '') => `${base}/${p}`.replace(/\/{2,}/g, '/')
193
230
  Astro's scoping attributes. Kept here so both /blog and /updates read
194
231
  the same. -->
195
232
  <style is:global>
233
+ /* Global because `html` sits outside Astro's scoping. Without it a jump
234
+ to a heading anchor lands under the sticky header. */
235
+ html {
236
+ scroll-padding-top: 4.5rem;
237
+ }
238
+
196
239
  .prose {
197
240
  color: var(--muted);
198
241
  line-height: 1.7;
@@ -16,9 +16,29 @@
16
16
  * Pairs with ThemeScript.astro in <head>, which is what actually avoids the flash;
17
17
  * this only handles clicks and keeps the icon honest.
18
18
  */
19
+
20
+ interface Props {
21
+ /**
22
+ * Pin the button to the top-right corner of the viewport.
23
+ *
24
+ * The default, because that is where it has always been and the landing lays
25
+ * out around it. `SiteShell` passes `false`: it puts the button in its own
26
+ * sticky header, and a fixed button over a fixed header is two things
27
+ * competing for the same corner — on a phone the floating one landed on top
28
+ * of the nav.
29
+ */
30
+ floating?: boolean
31
+ }
32
+
33
+ const { floating = true } = Astro.props
19
34
  ---
20
35
 
21
- <button id="theme-toggle" class="theme-toggle" type="button" aria-label="Toggle theme">
36
+ <button
37
+ id="theme-toggle"
38
+ class:list={['theme-toggle', { 'theme-toggle--floating': floating }]}
39
+ type="button"
40
+ aria-label="Toggle theme"
41
+ >
22
42
  <span class="icon" aria-hidden="true"
23
43
  ><svg
24
44
  xmlns="http://www.w3.org/2000/svg"
@@ -35,37 +55,67 @@
35
55
  </button>
36
56
 
37
57
  <style>
58
+ /* Quiet by default: no border, no fill, the same muted ink as the nav links
59
+ beside it. A bordered card-coloured box was the heaviest thing in a header
60
+ that is otherwise three grey words — it read as a primary control on a page
61
+ where changing the theme is the least important thing you can do. */
38
62
  .theme-toggle {
39
- position: fixed;
40
- top: 1rem;
41
- right: 1rem;
42
- width: 2.25rem;
43
- height: 2.25rem;
63
+ flex: none;
64
+ width: 2rem;
65
+ height: 2rem;
44
66
  display: grid;
45
67
  place-items: center;
46
- border: 1px solid var(--rule);
68
+ border: 1px solid transparent;
47
69
  border-radius: 8px;
48
- background: var(--card);
49
- color: var(--fg);
70
+ background: none;
71
+ color: var(--muted);
50
72
  cursor: pointer;
51
- z-index: 3;
52
73
  transition:
74
+ color 0.15s ease,
53
75
  border-color 0.15s ease,
54
76
  background-color 0.2s ease;
55
77
  }
56
78
 
79
+ /* Floating, it has no header to belong to and sits over arbitrary page
80
+ content, so it keeps the border and the fill — without them there is
81
+ nothing to say it is a button. */
82
+ .theme-toggle--floating {
83
+ position: fixed;
84
+ top: 1rem;
85
+ right: 1rem;
86
+ /* Above SiteShell's sticky header (z-index 10). Nothing pairs the two
87
+ today, but the whole point of a fixed button is that it stays reachable,
88
+ and the header would otherwise paint straight over it. */
89
+ z-index: 20;
90
+ width: 2.25rem;
91
+ height: 2.25rem;
92
+ border-color: var(--rule);
93
+ background: var(--card);
94
+ color: var(--fg);
95
+ }
96
+
57
97
  .theme-toggle .icon {
58
98
  display: grid;
59
99
  place-items: center;
60
100
  }
61
101
 
62
- .theme-toggle svg {
63
- width: 1.15rem;
64
- height: 1.15rem;
102
+ /* `:global` because the icon is swapped by innerHTML in the script below, and
103
+ what that writes carries none of Astro's scoping attributes. Scoped, these
104
+ rules applied only to the SVG the server rendered — the first `render()`
105
+ replaced it with one at Lucide's own 24px and stroke-width 2, which is why
106
+ the button looked so much heavier than the nav beside it. */
107
+ .theme-toggle :global(svg) {
108
+ width: 1.05rem;
109
+ height: 1.05rem;
65
110
  display: block;
111
+ stroke-width: 1.6;
66
112
  }
67
113
 
68
114
  .theme-toggle:hover {
115
+ color: var(--fg);
116
+ }
117
+
118
+ .theme-toggle--floating:hover {
69
119
  border-color: var(--faint);
70
120
  }
71
121
 
package/src/footer.css ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Styles for `components/SiteFooter.astro`.
3
+ *
4
+ * Its own file because the footer is the one piece of chrome both kinds of
5
+ * surface render: the three Starlight docs sites and the plain Astro ones
6
+ * (/blog, /updates, the landing). It used to live inside starlight.css, which
7
+ * is why /blog and /updates shipped a dot-separated link row instead — the
8
+ * shared footer would have rendered unstyled there.
9
+ *
10
+ * Imported by both `starlight.css` and `astro.css`; nothing needs to import it
11
+ * directly. Written against `--rx-*` tokens only, so it does not depend on the
12
+ * short aliases astro.css defines.
13
+ */
14
+
15
+ @import './tokens.css';
16
+
17
+ .rx-footer {
18
+ margin-top: 4rem;
19
+ padding: 3rem 0 2rem;
20
+ border-top: 1px solid var(--rx-rule);
21
+ color: var(--rx-muted);
22
+ font-size: 0.875rem;
23
+ }
24
+
25
+ /* The brand block and the link columns, side by side where there is room.
26
+ `auto-fit` rather than a media query because what matters is how wide the
27
+ footer's *container* is, not the viewport: on /blog the page measure is 44rem
28
+ on any screen, and a viewport query would have put the brand block beside the
29
+ columns and squeezed them to two cramped tracks on a desktop. */
30
+ .rx-footer__top {
31
+ display: grid;
32
+ grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
33
+ gap: 2.5rem 3rem;
34
+ }
35
+
36
+ .rx-footer__brand {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 0.75rem;
40
+ align-items: flex-start;
41
+ }
42
+
43
+ .rx-footer__mark {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ gap: 0.55rem;
47
+ color: var(--rx-fg);
48
+ font-size: 1rem;
49
+ font-weight: 640;
50
+ letter-spacing: -0.01em;
51
+ text-decoration: none;
52
+ }
53
+
54
+ .rx-footer__mark img {
55
+ display: block;
56
+ border-radius: 6px;
57
+ }
58
+
59
+ .rx-footer__blurb {
60
+ margin: 0;
61
+ max-width: 30ch;
62
+ color: var(--rx-faint);
63
+ line-height: 1.6;
64
+ }
65
+
66
+ /* 8rem, not the 11rem this used to be: it is the widest track that still fits
67
+ two columns on a 360px phone, and one column of five-word links down the
68
+ whole width of a phone is a lot of scrolling for a footer. */
69
+ .rx-footer__columns {
70
+ display: grid;
71
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
72
+ gap: 2rem 1.5rem;
73
+ }
74
+
75
+ .rx-footer__title {
76
+ margin-bottom: 0.75rem;
77
+ color: var(--rx-fg);
78
+ font-size: 0.75rem;
79
+ font-weight: 700;
80
+ letter-spacing: 0.06em;
81
+ text-transform: uppercase;
82
+ }
83
+
84
+ .rx-footer__list {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 0.45rem;
88
+ margin: 0;
89
+ padding: 0;
90
+ list-style: none;
91
+ }
92
+
93
+ .rx-footer__list a {
94
+ color: var(--rx-muted);
95
+ text-decoration: none;
96
+ }
97
+
98
+ .rx-footer__list a:hover {
99
+ color: var(--rx-fg);
100
+ text-decoration: underline;
101
+ text-underline-offset: 2px;
102
+ }
103
+
104
+ /* The bottom bar: copyright on the left, the legal links on the right, and one
105
+ column on a phone. */
106
+ .rx-footer__legal {
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: space-between;
110
+ flex-wrap: wrap;
111
+ gap: 0.5rem 1.25rem;
112
+ margin-top: 2.5rem;
113
+ padding-top: 1.25rem;
114
+ border-top: 1px solid var(--rx-rule);
115
+ color: var(--rx-faint);
116
+ font-size: 0.8125rem;
117
+ }
118
+
119
+ .rx-footer__legal p {
120
+ margin: 0;
121
+ }
122
+
123
+ .rx-footer__legal-links {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 1.25rem;
127
+ }
128
+
129
+ .rx-footer__legal a {
130
+ color: var(--rx-faint);
131
+ text-decoration: none;
132
+ }
133
+
134
+ .rx-footer__legal a:hover {
135
+ color: var(--rx-fg);
136
+ text-decoration: underline;
137
+ text-underline-offset: 2px;
138
+ }
package/src/starlight.css CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  @import './tokens.css';
19
+ @import './footer.css';
19
20
 
20
21
  :root {
21
22
  /* Accent. */
@@ -131,53 +132,6 @@ header.header {
131
132
 
132
133
  /* --- Footer -------------------------------------------------------------- */
133
134
 
134
- .rx-footer {
135
- margin-top: 4rem;
136
- padding: 2.5rem 0 2rem;
137
- border-top: 1px solid var(--rx-rule);
138
- color: var(--rx-muted);
139
- font-size: 0.875rem;
140
- }
141
-
142
- .rx-footer__columns {
143
- display: grid;
144
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
145
- gap: 2rem;
146
- }
147
-
148
- .rx-footer__title {
149
- margin-bottom: 0.75rem;
150
- color: var(--rx-fg);
151
- font-size: 0.75rem;
152
- font-weight: 700;
153
- letter-spacing: 0.06em;
154
- text-transform: uppercase;
155
- }
156
-
157
- .rx-footer__list {
158
- display: flex;
159
- flex-direction: column;
160
- gap: 0.45rem;
161
- margin: 0;
162
- padding: 0;
163
- list-style: none;
164
- }
165
-
166
- .rx-footer__list a {
167
- color: var(--rx-muted);
168
- text-decoration: none;
169
- }
170
-
171
- .rx-footer__list a:hover {
172
- color: var(--rx-fg);
173
- text-decoration: underline;
174
- text-underline-offset: 2px;
175
- }
176
-
177
- .rx-footer__legal {
178
- margin-top: 2.5rem;
179
- padding-top: 1.25rem;
180
- border-top: 1px solid var(--rx-rule);
181
- color: var(--rx-faint);
182
- font-size: 0.8125rem;
183
- }
135
+ /* Its styles are in footer.css, imported at the top of this file: /blog and
136
+ /updates render the same footer and load astro.css, not this one, so the
137
+ rules cannot live here. */