This project contains reusable Terraform modules for Vault configuration,
Proxmox virtual machines, backup storage, and related infrastructure. The
module collection is still in progress.
Provide one canonical DNS transformation for project-local Terraform resources
and offline inspection, preserving declared names, values, and view ownership.
Requirements
Requirement: Normalize canonical declarations without providers
The module SHALL accept decoded owner DNS documents and a zone, flatten every
supported type member, expand and deduplicate destinations, and expose one
normalized map without provider configuration. It SHALL reject unsupported
types, destinations, malformed members, and absolute names outside the zone.
Scenario: One logical declaration has several types and destinations
WHEN an entry contains A and AAAA with destinations all and global
THEN normalization produces exactly four records, one per type and view
Requirement: Preserve record multiplicity and stable identities
Normalized and provider resource keys SHALL use logical declaration key, type,
and view without mutable record values. Separate logical keys SHALL preserve
multiple records at the same name and type, including MX priority and TXT values.
Explicit TTLs SHALL override the type-specific compatibility defaults.
Scenario: A record changes address
WHEN an A member changes its address without changing its logical key
THEN its normalized and provider resource keys remain unchanged
Requirement: Keep provider ownership explicit
The module SHALL accept provider instances from its caller and create individual
Cloudflare global and RouterOS dc1 records only when explicitly enabled. It SHALL
default to disabled provider ownership while preserving normalized outputs.
Cloudflare records SHALL remain unproxied.
Disabled ownership SHALL NOT imply that operational provider initialization
or credential prerequisites are suppressed.
Scenario: A root prepares a future migration
WHEN a root calls the module without enabling ownership
THEN it declares no provider record resources and still exposes every normalized declaration
2 -
project-dns Specification
Purpose
Record the retirement of this project’s landing DNS infrastructure while
preserving its repository documentation and builds.
Requirements
Requirement: Keep landing infrastructure retired
The project SHALL have no dedicated landing DNS declaration, Terraform root, or
operational source export. Its landing page SHALL be published by the main site
under /projects/tf_modules/ instead of a dedicated hostname.
Scenario: Inspect the project after landing retirement
WHEN the project tree is consumed
THEN it contains no landing DNS declaration, Terraform DNS stage, or
landing build target, and the main site owns its landing page.
3 -
Reusable Terraform modules
Purpose
Provide reusable infrastructure declarations for Vault, virtual machines,
storage, and related services. This baseline was observed at repository
revision 550d7e79 on 2026-09-08. The collection remains in progress according
to its README. Sources for the
representative module contracts below are the
AppRole module,
transit-key module,
backup-bucket module, and
module packaging.
These are declarative source contracts; no live provisioning is asserted.
Requirements
Requirement: Package reusable module source
The AppRole, transit-key, backup-bucket, and Proxmox VM module packages SHALL
each expose their main.tf through a same-named Bazel filegroup available to
repository subpackages.
Scenario: A repository target consumes a module
WHEN a target depends on //projects/tf_modules/vault_approle:vault_approle
THEN its declared source input includes the AppRole module’s main.tf
Requirement: Associate AppRoles with Vault identities and policies
The AppRole module SHALL declare an identity entity, backend alias, internal
group, and named AppRole with configurable token and secret-ID limits. Its
role policies SHALL combine the shared and AppRole-secret policies with caller
policies and the Yandex-folder policy unless that policy is disabled for the role.
Scenario: Caller disables the Yandex-folder role policy
WHENdisable_yc_folder_policy is true
THEN the role’s policy list excludes the module’s Yandex-folder policy while preserving shared, AppRole-secret, and caller policies
Requirement: Separate transit encryption and decryption membership
The transit-key module SHALL declare a named Vault transit key, an encryption
policy and group, and a decryption policy and group. Decryptor member groups
SHALL also be included in the encryption group.
Scenario: A group is listed as a decryptor
WHEN a caller adds a group ID to decryptors_member_group_ids
THEN the module includes that group in both transit permission groups
The backup-bucket module SHALL declare a versioned Yandex storage bucket with
default KMS encryption, grant read and write permissions to the supplied
service accounts, and store bucket identifiers in the caller-selected Vault
KV v2 location.
Scenario: Backup storage is configured
WHEN the caller supplies the bucket name components, folder, service accounts, and Vault destination
THEN the declarations bind bucket encryption to the module’s KMS key and publish the bucket ID, folder ID, and bucket name to that Vault destination
4 - Backup bucket
S3 bucket for backups
5 - DNS records
Canonical DNS declarations for Cloudflare and RouterOS
This reusable module translates an owner’s decoded dnsconfig.json through
its provider-free normalizer, then manages individual
global Cloudflare and dc1 RouterOS records with caller-provided providers.
Owners with only global records use the global entrypoint,
which needs only Cloudflare. The combined entrypoint delegates Cloudflare
resources to that same module and consumes its normalization for RouterOS.
Repository infrastructure roots may consume its Bazel source filegroups;
the child normalizer also supports provider-free inspection.
zone defaults to alwaldend.com. An explicit cloudflare_zone_id is optional;
when it is null or empty, enabled global records resolve exactly one Cloudflare
zone matching zone. The token must permit zone listing and reading. Disabled
ownership and declarations without global records skip this lookup.
enabled defaults to false so a prepared root owns
no provider records. Keep it true after adoption: setting it false in a state
that already owns records plans deletion and is not a rollback procedure.
Disabling ownership does not suppress provider initialization. Operational
Terraform commands still require the caller’s real provider configuration
and the owning Vault/AppRole prerequisites. In particular, the pinned
RouterOS provider probes its configured API even when no DNS records are
enabled. Use the provider-free normalizer for offline inspection.
The migration runbook
owns cutover, import, and rollback ordering. This module supplies no provider
credentials or state backend.
The input contains exactly one records object. Each stable logical key
contains one or more scalar type members and a nonempty dsp list. Supported
destinations are global, dc1, and all; repeated destinations are deduplicated.
Type
Required member fields
Default TTL
A
name, address
300
AAAA
name, address
600
CNAME
name, target
600
NS
name, address
300
MX
name, target, priority
300
TXT
name, content
300
Every member accepts an integer ttl from 60 through 86400 seconds. MX
priorities range from 0 through 65535. Multiple scalar values at the same
name and type use distinct logical keys; changing values never changes
resource identity. Unsupported fields and malformed members are rejected.
Names accept @, relative names, or zone-qualified names. Absolute owner
names ending with a dot must be inside the zone. CNAME, MX, and NS targets
accept @, relative names, zone-qualified names, or absolute external names
ending with a dot. Normalization lowercases domain names and omits final dots;
address and TXT bytes are preserved. relative_name is @ at the apex.
normalized_records maps logical_key/type/view to key, name,
relative_name, type, value, priority (null except MX), ttl, view,
and proxied (always false). This output remains available while disabled.
The same keys index module.global.cloudflare_dns_record.records and
module.dc1[0].routeros_ip_dns_record.records in the combined entrypoint; the global-only
entrypoint uses cloudflare_dns_record.records. import_addresses exposes
these addresses relative to the selected module, including while staged.
The pinned providers import Cloudflare
records by zone_id/record_id and RouterOS records by static IDs such as *1A.
Terraform tests use mocked providers and pinned local provider packages.
The global-only test root has no RouterOS provider dependency. Normalization
tests require no providers. Both run without network access;
they establish declaration and resource mapping behavior, not live adoption.
5.1 - DNS normalization
Provider-free canonical DNS declaration transformation
This child module owns the canonical transformation described by the
DNS module. Its document and zone inputs produce
normalized_records without a provider, backend, or managed resources.
Terraform writers and provider-free inspection consume this output.
5.2 - DNS test providers
Pinned provider packages for offline Terraform tests
The test target uses the public
rules_terraform wrapper with
the same Cloudflare, RouterOS, and Proxmox provider labels as infrastructure
targets. The shared extension owns provider versions, release URLs, and
checksums. Bazel fetches the declared archives before tests execute; Terraform
uses the wrapper’s packaged filesystem mirror without a registry fallback.
Each regression runs Terraform in a caller-owned temporary module directory.
Tests retain an explicit environment without inherited provider credentials;
provider installation and mocked lifecycles require no network access.
The targeted import regression matches the owning Proxmox provider block by
omitting pm_api_url. It checks both empty state and existing Proxmox resource
state with no credentials. Without PM_API_URL, provider validation fails
before target pruning; supplying only a loopback endpoint permits the DNS
operation. The endpoint must receive zero requests.
The test imports a built-in Terraform resource through a root map into
module.dns, checks the exact no-op import, applies that saved plan, and verifies
the next targeted plan has no changes. Existing Proxmox resource attributes
remain unchanged. An untargeted control must fail authentication before
contacting the endpoint. These checks cover provider selection and state
preservation; live DNS adoption retains its separate inventory and plan checks.
5.3 - Global DNS records
Cloudflare-only entrypoint for canonical DNS declarations
Global-only owners use this entrypoint with the shared module API
to avoid requiring RouterOS configuration. The combined module also delegates
its Cloudflare resources here, so both entrypoints share one implementation.
Normalization returns all declared views, but this entrypoint owns only global
records. Mixed-view owners use the parent module to manage dc1 as well.
Enabled global records use the explicit cloudflare_zone_id when supplied.
Otherwise, the pinned cloudflare_zone data source resolves the configured
zone name and rejects missing or ambiguous matches. This requires zone-list
and zone-read access in addition to record permissions. Disabled ownership and
declarations without global records perform no zone lookup.
The optional zone ID must be known during planning so Terraform can select
whether discovery is needed. The repository’s AL injection supplies a known
value or an empty string before Terraform starts. Zone-name discovery uses the
normalizer’s canonical name, including case and final-dot handling.
import_addresses contains global addresses relative to this entrypoint.
record_ids exposes bound Cloudflare IDs for adoption and identity checks.
Publish every registered project’s visitor-facing landing page at
/projects// from the main site instead of a dedicated subdomain, and
retire the per-project Pages repository, CNAME, and Terraform DNS stage that
existed only to serve one page derived from the project README.
Each project owns its landing content in projects//site/content/ as pure
content: no layouts, styles, or build rules. The main site packages those
directories into content/projects// from the registry in
projects/projects.bzl, so membership is declared once. The landing is a short
hand-written page for visitors while the README keeps its /docs/projects//
URL for repository reference documentation, and landing front matter now feeds
the statuses, languages, and tags taxonomies.
Merge the reusable hugo_landing project into the main site: its shared canvas
and accent styles and its landing page rules move into the apex tree, and its
landing macro, generated configuration, standalone publisher, and
repository-relative rewrite layouts are removed. The theme keeps owning the
taxonomy and term layouts, which are richer than the heading-only versions the
reusable project carried. Remove the now-unused apex deploy_project.sh and
deploy_all.sh landing publishers with it.
Retire the per-project publication and DNS ownership: the project landing
targets and the landing deployment command, each landing project’s
dnsconfig.json and Terraform root, the build plumbing that served only those
stages, and the landing repository and Pages membership in the repository
catalog. The apex Terraform root is retained because it also declares VM
resources, and no Vault configuration is changed.
Replace the provider-snapshot zone files with generated declaration pages, one
per destination view, that project the checked-in declarations and are verified
for freshness by the DNS offline check. The DNS linter, both destination pages,
the apex site, the skills projection, and the repository quality suite pass, and
the whole repository builds.
Manage forge resources with hermetic Terraform rules
Centralize organization, repository, and named access configuration in
infra/repos. Adopt existing GitHub resources, protect master defaults while
retaining Pages publication on pages, and preserve existing repository
identities. Manage GitLab one-time imports, the F-Droid metadata fork, and
default-branch protections through Vault-backed authentication. Forgejo
consumes the same catalog. Ongoing repository synchronization is deferred.
Retire the eleven Bazel rule landing sites through their owning Terraform
workflows, removing their DNS records, Pages repositories, and landing
configuration. Move all twelve existing standalone rule modules into tools/
and retain their public interfaces and documentation on the main site.
Add tools/rules_terraform with verified provider downloads, packed mirrors
in runfiles, and reusable Terraform execution rules. Enforce one version per
provider source at an extensible resolution boundary. Migrate every Terraform
consumer to thirteen shared provider pins in third_party/terraform. Execution
and command maps belong to rules_terraform; consumers explicitly select generic
AL wrappers for authentication and Vault injection. Remove tools/terraform
and all 43 checked-in Terraform provider locks.
Validate installed providers against the declared archives before execution.
Validation covers all fourteen workspace builds, all thirteen standalone
test suites, Terraform consumers, offline real-provider regressions, rendered
documentation, formatting, and semantic lint. The full root test run has
295 passing tests and one skip; its only failure is the historical secret
scan, reduced to four pre-existing synthetic fixture matches. Earlier live
imports and the exact landing retirement scope have verified postconditions;
the Terraform rules migration uses only offline implementation checks.
Move DNS ownership into each project’s Terraform state using shared modules.
Load dnsconfig.json files at runtime, reject conflicting domain ownership,
and render their records as a table. Add missing project AppRoles and scoped
DNS policies, and document grouping related Terraform resources in modules.
Deploy all 45 owners sequentially: adopt 155 existing records and create 13
missing OpenHands records. Preserve every pre-existing provider record and
verify no-op follow-up plans and DNS answers. Add DNS-scoped plan/show/apply
wrappers with a saved-plan guard for roots that also manage services.
Allow bounded aggregate path inventories large enough to deliver this migration
while retaining truncation refusal in the delivery tool.
Validate runtime ownership, provider imports and preservation with isolated
fixtures, Terraform formatting, wrapper builds, repository quality checks,
semantic lint, and project documentation/specification builds.
Collapse OpenSpec source globs to specs and changes, drop redundant
package visibility blocks already covered by REPO.bazel, and regenerate
the affected catalogs.
LLM-disclaimer: This commit was generated by an LLM.
Publish project sites and reproducible infrastructure diagrams
Build and deploy all 30 remaining project sites with the standard Docsy
header, project descriptions and a linked documentation directory. Omit the
repeated page title and generated language heading. Remove
the unused cgit and xray_manager projects and retire their site resources.
Add a repeatable GitHub Pages publisher, staged Terraform bootstrap and direct
GitHub Pages DNS records. Preserve repeated publication when pages is the
remote default branch, and include Rules ISO in the main documentation.
Document site onboarding in a registered, validated skill. Keep AL
introduction short with useful links and preserve its lifecycle details in
a dedicated documentation page.
Render all 15 existing infrastructure Drawio pages with pinned Drawio and
Chrome inputs, sandboxed export actions, an update target and freshness
checks, and include the maintained diagrams in infra/arch documentation.
Paint an opaque white canvas to preserve readability in dark themes.
Validation covers project and apex builds, rendered pages, publisher tests,
diagram repeatability and freshness, DNS configuration, Terraform formatting,
semantic lint and repository quality.
Roll back partial plugin startup, drain requests before releasing resources,
and wait for plugin and command processes to exit. Stop independent plugins
concurrently while preserving dependency order inside each plugin.
Revoke invocation-owned credentials, remove temporary secret files, and verify
Forgejo and Harbor session cleanup. Preserve the user’s existing Vault login.
Enforce Vault OIDC origins and no_auth, redact sensitive errors, and restrict
config dumps to owner-only files. Add the AppRole SecretID cleanup permission.
Cover normal exit, cancellation, failed startup, credential revocation, and
transport cleanup with synthetic unit and real command/plugin regression tests.
Clarify that required formatting-only repairs need no separate approval, and
fix the existing blank-line formatting failure in the GitHub AL wrapper.
Extract the DNSControl setup into rules_dnscontrol, generate its Bazel
dependency bundle from project-owned record files, and update the DNS
guidance. Move Hugo rules and the worker to role-based package paths, and
give each non-nested project a locally owned Hugo landing target that reuses
the shared README-to-landing macro.
Clean all landing and Pages DNS records so GitHub Pages serves each project
subdomain directly without Cloudflare proxying. Improve the main site with
a three-panel home layout, complete project links, documentation explore
links, and current deployment guidance.
All affected root-workspace landing, DNS, and site targets pass, along with
the nested rules_hugo tests, Buildifier, and repository quality checks.
LLM-disclaimer: This commit was generated by an LLM.
Move docs_filegroup into a reusable rules_docs nested Bzlmod workspace and migrate every root and nested consumer to it. Add a module-aware Gazelle language that creates docs targets only for existing README packages while preserving manual mappings.
Wire the workspace into module resolution, documentation aggregation, and the ten-command full-repo-check. Add the bazel-nested-module skill so future nested workspaces follow the repository’s rc, lockfile, docs, toolchain, Gazelle, and validation conventions.
Keep reviewed Gazelle dependency canonicalization, while protecting the hand-maintained Go proto, Python resource, Android, embedded-site, and skill runner definitions that generic language plugins would otherwise break.
Restore light and dark site canvases and set the shared accent
The shared Docsy canvas set the same Bootstrap dark palette and the same
black page background on :root, [data-bs-theme="light"], and
[data-bs-theme="dark"], so the light/dark toggle changed nothing. The site
also rendered Bootstrap’s stock blue instead of its intended accent.
Split the shared palette so light mode uses a pure white canvas with the
light-mode palette and dark mode uses a pure black canvas with the dark-mode
palette, and keep the pre-stylesheet paint and footer on the active mode.
Assign $primary to #7c3aed before Bootstrap compiles, so links, buttons,
badges, focus rings, and the dark-mode link tint all derive from one value
rather than from a site-local copy.
Both files stay in the reusable shell; the apex site consumes them through
the existing declared exports and no longer carries the footer’s own blue.
Publish every registered project’s visitor-facing landing page at
/projects// from the main site instead of a dedicated subdomain, and
retire the per-project Pages repository, CNAME, and Terraform DNS stage that
existed only to serve one page derived from the project README.
Each project owns its landing content in projects//site/content/ as pure
content: no layouts, styles, or build rules. The main site packages those
directories into content/projects// from the registry in
projects/projects.bzl, so membership is declared once. The landing is a short
hand-written page for visitors while the README keeps its /docs/projects//
URL for repository reference documentation, and landing front matter now feeds
the statuses, languages, and tags taxonomies.
Merge the reusable hugo_landing project into the main site: its shared canvas
and accent styles and its landing page rules move into the apex tree, and its
landing macro, generated configuration, standalone publisher, and
repository-relative rewrite layouts are removed. The theme keeps owning the
taxonomy and term layouts, which are richer than the heading-only versions the
reusable project carried. Remove the now-unused apex deploy_project.sh and
deploy_all.sh landing publishers with it.
Retire the per-project publication and DNS ownership: the project landing
targets and the landing deployment command, each landing project’s
dnsconfig.json and Terraform root, the build plumbing that served only those
stages, and the landing repository and Pages membership in the repository
catalog. The apex Terraform root is retained because it also declares VM
resources, and no Vault configuration is changed.
Replace the provider-snapshot zone files with generated declaration pages, one
per destination view, that project the checked-in declarations and are verified
for freshness by the DNS offline check. The DNS linter, both destination pages,
the apex site, the skills projection, and the repository quality suite pass, and
the whole repository builds.
Manage forge resources with hermetic Terraform rules
Centralize organization, repository, and named access configuration in
infra/repos. Adopt existing GitHub resources, protect master defaults while
retaining Pages publication on pages, and preserve existing repository
identities. Manage GitLab one-time imports, the F-Droid metadata fork, and
default-branch protections through Vault-backed authentication. Forgejo
consumes the same catalog. Ongoing repository synchronization is deferred.
Retire the eleven Bazel rule landing sites through their owning Terraform
workflows, removing their DNS records, Pages repositories, and landing
configuration. Move all twelve existing standalone rule modules into tools/
and retain their public interfaces and documentation on the main site.
Add tools/rules_terraform with verified provider downloads, packed mirrors
in runfiles, and reusable Terraform execution rules. Enforce one version per
provider source at an extensible resolution boundary. Migrate every Terraform
consumer to thirteen shared provider pins in third_party/terraform. Execution
and command maps belong to rules_terraform; consumers explicitly select generic
AL wrappers for authentication and Vault injection. Remove tools/terraform
and all 43 checked-in Terraform provider locks.
Validate installed providers against the declared archives before execution.
Validation covers all fourteen workspace builds, all thirteen standalone
test suites, Terraform consumers, offline real-provider regressions, rendered
documentation, formatting, and semantic lint. The full root test run has
295 passing tests and one skip; its only failure is the historical secret
scan, reduced to four pre-existing synthetic fixture matches. Earlier live
imports and the exact landing retirement scope have verified postconditions;
the Terraform rules migration uses only offline implementation checks.
Move DNS ownership into each project’s Terraform state using shared modules.
Load dnsconfig.json files at runtime, reject conflicting domain ownership,
and render their records as a table. Add missing project AppRoles and scoped
DNS policies, and document grouping related Terraform resources in modules.
Deploy all 45 owners sequentially: adopt 155 existing records and create 13
missing OpenHands records. Preserve every pre-existing provider record and
verify no-op follow-up plans and DNS answers. Add DNS-scoped plan/show/apply
wrappers with a saved-plan guard for roots that also manage services.
Allow bounded aggregate path inventories large enough to deliver this migration
while retaining truncation refusal in the delivery tool.
Validate runtime ownership, provider imports and preservation with isolated
fixtures, Terraform formatting, wrapper builds, repository quality checks,
semantic lint, and project documentation/specification builds.
Collapse OpenSpec source globs to specs and changes, drop redundant
package visibility blocks already covered by REPO.bazel, and regenerate
the affected catalogs.
LLM-disclaimer: This commit was generated by an LLM.
Publish project sites and reproducible infrastructure diagrams
Build and deploy all 30 remaining project sites with the standard Docsy
header, project descriptions and a linked documentation directory. Omit the
repeated page title and generated language heading. Remove
the unused cgit and xray_manager projects and retire their site resources.
Add a repeatable GitHub Pages publisher, staged Terraform bootstrap and direct
GitHub Pages DNS records. Preserve repeated publication when pages is the
remote default branch, and include Rules ISO in the main documentation.
Document site onboarding in a registered, validated skill. Keep AL
introduction short with useful links and preserve its lifecycle details in
a dedicated documentation page.
Render all 15 existing infrastructure Drawio pages with pinned Drawio and
Chrome inputs, sandboxed export actions, an update target and freshness
checks, and include the maintained diagrams in infra/arch documentation.
Paint an opaque white canvas to preserve readability in dark themes.
Validation covers project and apex builds, rendered pages, publisher tests,
diagram repeatability and freshness, DNS configuration, Terraform formatting,
semantic lint and repository quality.
Roll back partial plugin startup, drain requests before releasing resources,
and wait for plugin and command processes to exit. Stop independent plugins
concurrently while preserving dependency order inside each plugin.
Revoke invocation-owned credentials, remove temporary secret files, and verify
Forgejo and Harbor session cleanup. Preserve the user’s existing Vault login.
Enforce Vault OIDC origins and no_auth, redact sensitive errors, and restrict
config dumps to owner-only files. Add the AppRole SecretID cleanup permission.
Cover normal exit, cancellation, failed startup, credential revocation, and
transport cleanup with synthetic unit and real command/plugin regression tests.
Clarify that required formatting-only repairs need no separate approval, and
fix the existing blank-line formatting failure in the GitHub AL wrapper.
Extract the DNSControl setup into rules_dnscontrol, generate its Bazel
dependency bundle from project-owned record files, and update the DNS
guidance. Move Hugo rules and the worker to role-based package paths, and
give each non-nested project a locally owned Hugo landing target that reuses
the shared README-to-landing macro.
Clean all landing and Pages DNS records so GitHub Pages serves each project
subdomain directly without Cloudflare proxying. Improve the main site with
a three-panel home layout, complete project links, documentation explore
links, and current deployment guidance.
All affected root-workspace landing, DNS, and site targets pass, along with
the nested rules_hugo tests, Buildifier, and repository quality checks.
LLM-disclaimer: This commit was generated by an LLM.
Move docs_filegroup into a reusable rules_docs nested Bzlmod workspace and migrate every root and nested consumer to it. Add a module-aware Gazelle language that creates docs targets only for existing README packages while preserving manual mappings.
Wire the workspace into module resolution, documentation aggregation, and the ten-command full-repo-check. Add the bazel-nested-module skill so future nested workspaces follow the repository’s rc, lockfile, docs, toolchain, Gazelle, and validation conventions.
Keep reviewed Gazelle dependency canonicalization, while protecting the hand-maintained Go proto, Python resource, Android, embedded-site, and skill runner definitions that generic language plugins would otherwise break.
Restore light and dark site canvases and set the shared accent
The shared Docsy canvas set the same Bootstrap dark palette and the same
black page background on :root, [data-bs-theme="light"], and
[data-bs-theme="dark"], so the light/dark toggle changed nothing. The site
also rendered Bootstrap’s stock blue instead of its intended accent.
Split the shared palette so light mode uses a pure white canvas with the
light-mode palette and dark mode uses a pure black canvas with the dark-mode
palette, and keep the pre-stylesheet paint and footer on the active mode.
Assign $primary to #7c3aed before Bootstrap compiles, so links, buttons,
badges, focus rings, and the dark-mode link tint all derive from one value
rather than from a site-local copy.
Both files stay in the reusable shell; the apex site consumes them through
the existing declared exports and no longer carries the footer’s own blue.