App Footer
How an app page ends, in two weights: the 24px stone rail, or the app frame's beta bar — both in place of the marketing footer.
When to Use
WS_APP_PATHS gets this automatically, whichever way it reaches the footer. Call ws_app_footer() directly only to force the rail on a page outside those paths.
Variants
band (default) is the 24px rail: one height, one colour, no content. A tool page should end quietly, and anything configurable there would be an invitation to put marketing back.
<?= ws_app_footer() ?>
full is the consolidated shell's frame bar, from the app wireframes: a 56px --gray-500 band carrying the wordmark, the 2.0 Beta pill and the build version (read live from the changelog's newest entry) on the left, and Send feedback / Resources / Company on the right. An Install the app action sits hidden before them, revealed only when Chrome fires beforeinstallprompt — the installed app is the one window a browser draws without a URL row, so the bar offers it exactly when the browser permits (Safari and Firefox never fire the event and never see the button). Send feedback opens a ws_modal wired to api/contact.php; Resources and Company open small link panels above their buttons, carrying the same links as the marketing footer's columns. The launcher has no footer entry point — the rail's More button is its one way in. The composite's automatic routing calls with no arguments, so the shell picks this variant through $GLOBALS['ws_app_footer_options'] before the footer runs. Full-bleed tool steps take no footer at all (DESIGN.md §12.7).
// app/index.php — before includes/footer.php runs $GLOBALS['ws_app_footer_options'] = ['variant' => 'full'];
How a page gets it
Every route to a footer — ws_footer(), a direct components/footer.php include, or the legacy includes/footer.php — passes through the composite template. That template runs one test and delegates, so no page needs per-page wiring and no app can drift.
// includes/components/footer.php
if (ws_is_app_page()) {
include __DIR__ . '/app-footer.php';
return;
}ws_is_app_page() matches the first segment of the request path against WS_APP_PATHS, which mirrors the app inventory in PRODUCT.md §5:
| Phase | Paths |
|---|---|
| Learn | library, academy, tips, podcasts |
| Plan | planner, agenda-check, toolkits, myworkshopr |
| Facilitate | facilitator, sidekick, intervention |
| Reflect | dna, synthesize, coach |
| Other | strategist, admin |
Adding an app means adding its path there and nothing else. Don't add a marketing page that happens to live under an app directory.
States
| State | Behavior |
|---|---|
| First render | Defines WS_GLOBAL_FOOTER_RENDERED, emits the token link, the style block, the rail, and the toast/auth functional core. |
| Subsequent renders | The shared guard returns early. A page gets one footer of one kind — whichever composite reaches the guard first wins. |
| Short page | The rail sits in normal flow, so on a page shorter than the viewport it lands under the content rather than at the window bottom. It does not stick. |
| Fixed-viewport page | Not rendered. The Planner builder, facilitator/app.php and synthesize/app.php are 100vh workspaces — 24px more would buy a scrollbar on a canvas designed not to scroll, and the rail would sit below the fold. Those pages end at </body>. |
Options
The helper takes no arguments. If you find yourself wanting one, the page probably wants the full Footer instead.
Accessibility
| Concern | Behavior |
|---|---|
| Landmark | Carries aria-hidden="true". The rail holds no content, and an empty contentinfo landmark is noise a screen-reader user has to check and discard. |
| Contrast | Not applicable — there is no text. --gray-400 is documented as decorative-only for exactly this reason; never put essential text on it. |
| Focus | Nothing focusable, so it adds no tab stops between the last control on the page and the end of the document. |
Tokens
| Token | Value | Use |
|---|---|---|
--gray-400 | #A8A29E | The rail's fill — the only colour in the component |
The component emits platform-tokens.css itself, because app pages such as the Planner and Facilitator run their own CSS stacks and don't all load the shared sheet. Browsers dedupe the identical href. See Token Reference for current values.
CSS Classes
| Class | Purpose |
|---|---|
.ws-app-footer | The rail. 24px tall, flex: none so a flex page layout can't compress it. |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_app_footer(), ws_is_app_page(), and the WS_APP_PATHS list |
includes/components/app-footer.php | The template, the shared render-once guard, and the bundled style |
includes/components/footer.php | The composite that delegates here on an app page |
Live render
The real component follows, full-bleed. The stone band below this line is the whole thing.