MeowPy Community is a full Discourse theme with paired light and dark
color schemes. It uses #1781B5 as the shared interaction anchor in both
modes, with neutral surfaces and semantic colors doing most of the visual
work.
The layout has three deliberate surface treatments:
- Latest starts with a transparent header and sidebar rail so the navigation shares the page canvas; the sticky header gains the chrome surface after the welcome area collapses, while topic pages keep the chrome header from the start;
- the topic list is a single-column feed of horizontal discussion cards;
- Latest adds an unframed recommendation rail on wide screens;
- topic details remain a continuous, non-card reading surface with the Discourse timeline and composer intact.
On desktop screens the sidebar is the first track of the same centered layout that contains the topic content and recommendation rail. On Latest, the sidebar starts beside the list and recommendation rail, below the independent welcome/search area. That upper area expands across the reserved sidebar track while remaining centered against the viewport. At medium desktop widths the reserved track narrows so the navigation never covers the topic list.
On desktop topic-list pages, Discourse's native category, tag, and sort controls are placed in a compact row directly below the topic table headings and above the first topic. The sidebar keeps the native new-topic action at the top of its rail, and begins below the welcome area so it shares the recommendation rail's visual starting point.
Import this directory as a Discourse theme, then select either MeowPy Light
or MeowPy Dark as the active color scheme. To enable light/dark/auto
switching, pair the schemes in the admin theme settings: keep MeowPy Light
as the theme's color scheme and assign MeowPy Dark as its dark color
scheme. Discourse then serves both palettes and follows the chosen mode.
With both schemes paired, the theme adds a color-mode selector at the bottom
of the sidebar, in the footer actions row. Opening it shows a compact menu
with light, dark, and auto options; the current mode is highlighted and the
trigger's icon reflects it. The control is styled after the interface color
selector that Discourse ships in core, but implemented by the theme itself.
It delegates to Discourse's native interface color service: auto follows the
system prefers-color-scheme setting, and the choice persists in the same
cookie core uses. If the site has only one scheme, the control stays out of
the sidebar. Because the selector is provided by the theme, the core
interface color selector site setting does not need to be enabled; leaving
it off avoids two selectors appearing side by side.
Mode changes are softened with motion: each switch wraps the stylesheet flip in a one-shot palette fade so the whole page cross-fades over the theme's slow motion token (skipped under reduced motion), and the menu enters with the shared dropdown animation already defined for every other surface.
The theme adds no dependencies or custom routes.
Its JavaScript uses supported Discourse theme APIs for the integrations below, plus a small desktop DOM observer that keeps the native topic filters in their table position after route updates:
- a sidebar footer selector offering light, dark, and auto interface color
modes through a menu, driven entirely by Discourse's
interface-colorservice; - page-transition motion reserved for context crossings: leaving or arriving
at the outer list layer (home, latest, categories, tags) and content-to-
content switches animate; the permanently instant lanes are list-to-list
hops, topic-to-topic binge-reading, and same-signature repeats inside
1.2s. On capable desktops the crossing plays the banner carousel's own
vocabulary (zoom entrance, back easing on the banner, tightened to 600ms)
through the View Transitions API via a router-service hook that keeps the
header and sidebar perfectly still; the banner departs and arrives through
a corner-anchored circular mask (a 150%-radius
clip-path: circle(), sized to actually cover the far corner) that animates only clip-path — no opacity, no geometry — with a short start delay so the mask's clock begins after the route's capture/render storm has drained, then drains or opens over a tier-tuned ~300–320ms. Departures are narrated in two beats: the mask closes to half its footprint first, and only at that halfway mark does the content region hand off (old fades out, new zooms in) while the mask finishes draining alongside; when renders run slow (measured via a rolling average), the same entrance is instead bridged by dimming the outgoing page in the click frame and lifting it on arrival, so the animation can never be truncated by the server. Device tiers soften or skip the effects (no View Transitions on low-memory/low-core or save-data devices, softened timings on touch), Safari and all other Apple-WebKit browsers are routed to the dim bridge because WebKit's same-document View Transitions still flicker and stall on full-page snapshots (escape hatch:localStorage["meowpy-page-transitions-force-vt"] = "on"), group geometry never animates, the unnamed shell stays static, reduced motion skips everything, andlocalStorage["meowpy-page-transitions"] = "off"disables it all. The animation level itself is an admin-only theme setting (motion_mode, default "reduced") that applies site-wide: every visitor rides the same grammar, there is no per-user override anywhere in the client, and Apple-WebKit devices plus hardware that tiered down to lite always run the fades regardless of the setting. During transitions the outgoing content and sidebar snapshots are measured against the rendered destination and translated back onto their true origins, so route pairs whose geometry differs (banner leaving or joining the layout, mid-list scroll positions) never read as the list or column jumping over the banner or snapping to the viewport top. Every armed navigation also remembers where the departing page sat and opens a short restoration session on arrival: each frame it re-asserts the remembered list offset (or the top, for forward entries) until it holds steady past the transition, so async height collapses — which make the browser clampscrollYnatively mid-navigation — can no longer undo the restore; redirect chains refine the target instead of consuming it ("/" and "/latest" are remembered as one list); browser back restores too; and the transition's snapshot is only taken once the scroll has converged, so returning to a scrolled list never flashes the unscrolled viewport first. Alignment pairing waits for the same convergence plus one rendered frame after page change, so the ghosts are never measured against the outgoing route's stale DOM; - a native
CreateTopicButtonin the sidebar with category/tag context; - desktop use of Discourse's existing navigation dropdown;
- a five-item Latest recommendation rail populated from Discourse's Hot, Top, and Latest endpoints, in that priority order with duplicate topics removed. Results are cached in memory for five minutes and shared across remounts, so repeated visits to Latest do not refetch the endpoints.
Typography uses the font selected by the Discourse site. The theme does not load a remote font or override an administrator's typography choice.
discourse-banner-carousel is compatible with this theme without a layout
override. The plugin renders its Ember carousel through Discourse's verified
below-site-header Plugin Outlet. In the current Discourse application
template (2026.8.0-latest.1), that outlet is rendered after the optional
welcome banner and before <div id="main-outlet-wrapper">. The carousel is
therefore outside the sidebar/content/recommendation grid on every route and
does not become a child of .has-sidebar-page #main-outlet-wrapper.
The theme intentionally does not style the carousel or modify
.welcome-banner, .list-controls, .sidebar-wrapper, or
#main-outlet-wrapper. The plugin stylesheet uses dbc--prefixed selectors
and reads the theme's existing --meowpy-surface, --meowpy-text,
--meowpy-border, and --meowpy-radius tokens, with native Discourse color
tokens as fallbacks. The light and dark MeowPy schemes consequently provide
the carousel's surface, text, border, and accent colors automatically.
The application-level outlet remains mounted during Ember route transitions, so only one carousel instance is rendered while its route-aware data is updated. Do not add a second connector in this theme; doing so would create a duplicate carousel and would place it inside the three-column content area.
| Role | Value |
|---|---|
| Primary text | #20333D |
| Page background | #FFFFFF |
| Surface | #FFFFFF |
| Primary interaction | #1781B5 |
| Secondary blue | #4CA4D6 |
| Selected surface | #E9EEF1 |
| Teal accent | #2FA58D |
| Coral accent | #E16F62 |
| Rose accent | #C45F82 |
| Role | Value |
|---|---|
| Primary text | #DCE8EE |
| Page background | #11181D |
| Surface | #151E24 |
| Primary interaction | #1781B5 |
| Accessible text blend | brand/text contrast blend |
| Highlight base | #1D282E |
| Teal accent | #53BFA7 |
| Coral accent | #EF8177 |
| Rose accent | #D77898 |
Dark mode keeps the same #1781B5 interaction anchor. Text links use a
small contrast-safe blend with the mode's primary text, while hover and
selected surfaces are neutral mixes of the surface and text colors. The
native Discourse hover, selected, and highlight tokens are also mapped
to those neutral surfaces so modernized list controls do not reintroduce pale
blue highlights. Large surfaces stay neutral in both modes; category colors
are reserved for small icons, markers, tags, and semantic states.
In light mode, the exact #1781B5 brand color is used for markers, state
accents, and solid buttons. Small text links mix that brand value with the
mode's primary text color so their contrast remains readable. Third-party
color schemes can re-skin every brand-derived color by overriding
--meowpy-brand-override before the theme tokens resolve.
Animation follows one small vocabulary declared as custom properties at the
top of common/common.scss: three durations and two curves. The intent is a
community that feels young but trustworthy — professional and quiet, with a
pulse of energy — so motion is punctuation, not performance.
| Token | Value | Use |
|---|---|---|
--meowpy-motion-fast |
120ms |
hover, focus, and press state shifts |
--meowpy-motion-base |
180ms |
state changes on larger surfaces |
--meowpy-motion-slow |
320ms |
entrances and expressive swaps |
--meowpy-motion-ease-standard |
ease |
the calm default |
--meowpy-motion-ease-emphasized |
cubic-bezier(0.34, 1.56, 0.64, 1) |
slight overshoot, accent moments only |
Rules every animation in the theme must follow:
- Durations stay under 350ms and always come from these tokens; no ad-hoc values.
- Only opacity, the individual transform properties (
translate,rotate,scale), colors, borders, and backgrounds animate — never layout properties such as width, height, or top. - Entrances travel at most 6px; button presses contract at most 2%.
- Every effect must degrade gracefully under
prefers-reduced-motion: reducevia the global override at the bottom ofcommon/common.scss.
Where motion appears today: interactive surfaces (sidebar links, header icons, nav controls, post actions, buttons) settle their hover and focus colors with the fast duration; topic-list rows settle at the base duration; buttons contract slightly while pressed; dropdown and select-kit panels enter with a quiet fade-and-settle; the recommendation rail's skeletons sweep a soft highlight while loading and its entries rise in with a short stagger once loaded; the rail's accent dot emits a slow, faint ping to mark live content; and the color-mode trigger cross-fades through a rotation using the emphasized curve.
One piece of core motion is intentionally removed: Discourse fades the
heading anchor icon (the link glyph beside a post heading on hover) in from
opacity: 0 over 250ms. The icon is a content-image ::before positioned
outside the zero-width inline anchor, and starting that fade promotes a fresh
compositor layer mid-hover whose first frames can paint the icon partially —
half a glyph, or a box stretched to the heading's line-height — so it appears
to render "one half first, then the other half". The theme sets
transition: none on .cooked/.d-editor-preview heading anchors so the
icon paints once, fully formed; core keeps ownership of when it shows.
The theme targets current evergreen browsers: Chrome/Edge 111+, Firefox 121+,
and Safari 16.4+. Its layout relies on :has() selector support, color-mix()
for token derivation, scrollbar-gutter, and range syntax in media queries.
Older engines are not supported and may fall back to Discourse's base styles
in places.
- Topic cards use thin borders and no elevation. They should read as discussions, not SaaS widgets.
- Desktop sidebar groups use whitespace instead of nested panels, accent stripes, shadows, or footer fades. The mobile drawer keeps a solid surface because it overlays the reading area.
- Navigation dropdowns and category/tag comboboxes share one compact control vocabulary and use borders without shadows.
- The desktop sidebar, content grid, and banner share one responsive maximum width. They follow the viewport at ordinary desktop sizes, then remain centered with equal outer gutters on wide screens; reading text keeps its own line-length cap, and the recommendation rail disappears before it would crowd the topic table.
- Topic posts, headers, and quotes never become independent floating cards.
- Mobile keeps the card hierarchy while allowing Discourse to collapse its desktop navigation and timeline normally.