# ADR 0001: Styling approach for the feed browser redesign - Status: Proposed (waiting for Cesar's sign-off on CRM task `1ublul6nq`) - Date: 2026-10-19 - Source: `docs/minnect-feed-redesign.spec.md`, sections "Design tokens", "Typography", "Buttons", "Desktop and tablet layout", "Theme" and "Implementation notes". Where this ADR and the spec disagree, the spec wins and this ADR is fixed. ## Context The redesign changes every screen, style and motion of the feed browser. The data layer (`src/lib/*`, `src/app/api/*`) does not change. The app today: - Next.js 15.5 App Router, React 19. No UI library, no CSS-in-JS, no Tailwind. Dependencies are `next`, `react`, `react-dom` and `hls.js`. - One stylesheet, `src/app/globals.css` (128 lines), imported once in `src/app/layout.tsx`. It uses a few light-only custom properties (`--red`, `--ink`, `--grey`, `--line`, `--bg`), viewport media queries at 959px, and broad element rules such as `button { background: var(--red); color: #fff; margin-top: 16px }` that leak into every button. - Roboto is loaded with `next/font/google` and applied as a class on ``. - There is no theme and no dark mode. The approved mockup (canvas file `Main.dc.html`) holds working CSS for every component. It also holds the canvas runtime that draws the boards, which is not part of the app. Several build tasks (tokens and theme, typography, layout, buttons, each component) will write CSS in parallel. They need one set of rules so their work fits together. ## Decision ### 1. Tokens: CSS custom properties with a dark set and a light set - Every color in the spec's token table (`--bg`, `--tile`, `--tile-2`, `--tile-3`, `--line`, `--line-2`, `--ink`, `--ink-2`, `--ink-3`, `--accent`, `--danger`, `--glass`) is a custom property. Values are copied exactly from the spec. - The dark set is declared on `:root` and on `:root[data-theme="dark"]`. The light set is declared on `:root[data-theme="light"]`. Dark is therefore the default even if the theme attribute is missing. - Crimson shades are derived from `--accent` with `color-mix()` and declared once next to the tokens: `--accent-hi` (26% white), `--accent-lo` (30% black), `--accent-soft` (88% transparent). - Shared non-color values are tokens too: radii (`--r-panel: 18px`, `--r-card: 16px`, `--r-ctl: 12px`), the standard ease (`--ease: cubic-bezier(.2,.8,.2,1)`), and the font families (section 4). - Components use only tokens. A hex, `rgb()` or named color inside a component file is a review failure. The one exception is pure white or black inside crimson buttons, sparks and media fades, where the spec asks for white. - No gold or champagne value may appear anywhere. - The old tokens (`--red`, `--grey`) are removed, not kept as aliases. ### 2. Plain CSS, no UI library - Styles are plain global CSS files. No component library, no Tailwind, no CSS-in-JS, no Sass, no CSS Modules. - Reason for global classes rather than CSS Modules: the mockup CSS is written as global class rules, and porting it unchanged (section 7) is the fastest and safest route. Collisions are avoided by naming instead: each component's classes start with its own prefix (for example `.post-*`, `.composer-*`, `.featured-*`, `.trending-*`, `.search-*`, `.dock-*`, `.sheet-*`, `.btn-*`). - No new npm dependency is added for styling. ### 3. Layout responds to container queries on the app root - The signed-in app has one root element, `.app`, with `container: app / inline-size`. The bento grid is its child (`.bento`), because a container query cannot style the container itself. - Breakpoints are written as `@container app (min-width: 1181px)`, `(1001px to 1180px)`, `(761px to 1000px)` and `(max-width: 760px)`, matching the spec's four layouts. - Viewport width media queries (`@media (max-width: ...)`) are not used for layout. The existing 959px media queries are deleted with the old stylesheet. - Media queries stay allowed for things that are not width: `prefers-reduced-motion`, `hover` and `pointer`. `prefers-color-scheme` is not used, because dark is the default for a first visit regardless of the system setting (section 5). - The page height is fixed to the viewport (`height: 100vh`, with `100dvh` where supported so mobile browser bars do not cause a page scroll). `body` never scrolls. Only the feed list, and the panels the spec names, scroll. - The sign-in page is not inside `.app`. It is a single centered column and needs no breakpoints. ### 4. Fonts through `next/font/google` - `src/app/layout.tsx` loads `Manrope` (weights 400 to 800) and `Cormorant_Garamond` (500, 600, and italic 500) from `next/font/google`, each with the `variable` option: `--font-sans` and `--font-serif`. - Both variable classes go on ``. `body` uses `font-family: var(--font-sans), system-ui, sans-serif`. Title classes use `var(--font-serif)`. - Roboto is removed. No font is loaded from a `` tag or a CDN, so fonts are self-hosted by Next and there is no layout shift from a late font. ### 5. Theme: stored in localStorage, applied before first paint - The theme is the attribute `data-theme="dark" | "light"` on ``. - A small inline script is the first child of `` in `src/app/layout.tsx`, written with `dangerouslySetInnerHTML`. It runs before the body is parsed and before first paint: - read `localStorage.getItem("theme")` inside try/catch; - if the value is `"light"` or `"dark"`, use it, otherwise use `"dark"`; - set `document.documentElement.dataset.theme` to that value. - The script is a fixed string with no user input in it. It contains no token, session id or secret (CLAUDE.md rule 1). - `` gets `suppressHydrationWarning`, because the server renders it without the attribute value the script sets. - The server renders `data-theme="dark"` on `` so that a browser with scripts off still gets the default dark theme. - The three toggles (account panel, mobile top bar, Discover sheet switch) call one shared helper that sets the attribute and writes `localStorage.setItem("theme", value)` inside try/catch. A failed write still changes the theme for the session. - Changing theme only swaps token values. No component has theme-specific rules, except where the spec gives a component a different look per theme. Those rules are written as `:root[data-theme="light"] .component { ... }` inside that component's file. - `color-scheme` is set to `dark` or `light` with the theme, so native controls and scrollbars match. - Rejected: reading the theme in React state and rendering after mount (it flashes the dark theme to light users on every load), and a theme cookie read on the server (it adds server work and a cookie for a purely visual choice). ### 6. How `globals.css` is split by component `src/app/globals.css` keeps only what is global. Component rules move to one file per component in `src/styles/`: | File | Holds | |---|---| | `src/app/globals.css` | `@layer` order, tokens (both themes), the reset and element defaults, focus ring, `prefers-reduced-motion` switch-offs that apply everywhere | | `src/styles/layout.css` | `.app`, `.bento`, panels, the four container-query layouts, the mobile full-screen scroller frame | | `src/styles/buttons.css` | The five button styles and the glossy crimson icon button | | `src/styles/avatar.css` | `HexAvatar` | | `src/styles/post.css` | Post card, menu and delete confirm, like, media, video play | | `src/styles/comments.css` | Comments list, empty and removed states, comment field | | `src/styles/composer.css` | Composer, toolbar, emoji popover, attachment row | | `src/styles/featured.css` | Featured experts carousel | | `src/styles/trending.css` | Trending minnects | | `src/styles/search.css` | Find experts, chips, results | | `src/styles/mobile.css` | Mobile slides, top bar, dock, sheets, heart burst | | `src/styles/signin.css` | Sign-in page | - `src/app/layout.tsx` imports `globals.css` first and then each file in the table order. All of them are global stylesheets, which the App Router allows in the root layout. - A file that grows past about 600 lines is split again by sub-component. A new component gets its own file rather than growing an unrelated one. - A build task edits only the files for the components it owns, plus `globals.css` when it adds a token. ### 7. Porting the mockup CSS - Copy the CSS rules for each component from the mockup source (`Main.dc.html`) into that component's file. Keep the selectors, values, keyframes and transitions as they are, then: - replace any literal color with the matching token; - rename classes to the component's prefix where they would collide; - turn any board-sized or fixed-width rule from the mockup into the container-query layout in section 3. - Do not copy the canvas runtime: no board frames, artboard positioning, canvas scripts, demo state switches, sample data or the canvas's own fonts and wrappers. Interactive behavior the mockup fakes with script (carousel rotation, double-tap like, sheets, emoji insertion, theme toggle) is written in React in the component, not lifted from the canvas. - The uiverse.io button styles come into the app through the mockup's already recolored rules, not from uiverse.io directly. ### 8. Cascade order and the `button { color: inherit }` rule - `globals.css` starts with `@layer reset, base;`. The reset and every element rule (`button`, `input`, `textarea`, `a`, `h1` to `h3`, `ul`) live in those layers. Component files are not layered. - Unlayered rules always win over layered ones, whatever the specificity. So a component class always beats `button { color: inherit; background: none; border: 0; font: inherit }` from the reset. - In addition, every button class sets its own `color` explicitly for each state it styles (rest, hover, disabled, busy). No button style relies on inheriting its text color. This is the fix for the mockup bug where the Post button showed ivory text on an ivory face. - The element rules in the reset are neutral only: no background color, no margin, no padding beyond 0. The current global `button` rule (crimson background, white text, 16px top margin) is removed with the old stylesheet. - `!important` is not used, except inside the `prefers-reduced-motion` block that switches animations off. ## Consequences - Build tasks can work in parallel: each owns its own stylesheet and uses the shared tokens, so the two themes stay consistent without per-component theme rules. - The theme script is the only inline script in the app. If a Content Security Policy is added later, it needs a hash or nonce for this script. - Container queries need current browsers (Chrome 105+, Safari 16+, Firefox 110+). Older browsers get the mobile layout because no container rule applies. This is accepted for this app. - `color-mix()` needs Chrome 111+, Safari 16.2+, Firefox 113+. Accepted on the same terms. - Global classes need naming discipline. The prefix rule and the one-file-per-component rule are what keep it working, and reviewers check them. - The old stylesheet is replaced, not refactored. Until every component task is merged, unported screens may look unstyled on the milestone branch. This is expected and is not a regression to report. - Removing Roboto and the old tokens touches `layout.tsx` and every component's class names. That work belongs to the build tasks (`3p1idvga1` tokens and theme, `wy86rbj5i` typography, and the component tasks), not to this ADR.