Why most of the portfolio breaks on a phone, the one root cause behind it, and a single stylesheet every app can share to fix it — explained from first principles.
boom-responsive.cssEvery app rated "BROKEN ON MOBILE" shares a single technical trait. Every app rated GOOD avoids it.
There are two ways to style a React component. One of them can adapt to screen size. The other physically cannot. Most of the portfolio uses the one that can't — and that single choice explains nearly every mobile defect found in the audit.
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)'
}}>
Always 4 columns. On every screen. Forever. There is no syntax in an inline style object for "…but 1 column on a phone." The feature does not exist.
<div className="grid
grid-cols-1
md:grid-cols-4">
1 column by default, 4 columns once the screen is ≥768px wide. The md: prefix is a media query — the mechanism that makes responsiveness possible at all.
A media query is a rule that says "apply these styles only under these conditions" — usually a screen width. It looks like this in plain CSS:
/* Only applies when the screen is 640px wide or narrower */
@media (max-width: 640px) {
.my-grid { grid-template-columns: 1fr; }
}
Media queries live in stylesheets. Inline style={{}} objects are attached directly to a single HTML element, and an element has no concept of "conditions" — it just has values. So the moment a layout is written inline, its responsiveness is not "incomplete," it is architecturally impossible without changing the approach.
Take Posttee's dashboard shell, which is gridTemplateColumns: '220px 1fr' written inline:
The 220px sidebar is hardcoded, so it consumes 59% of the screen before a single pixel of content renders. The user gets ~155px to work in. The same defect exists — at 210px, 216px, 220px, 240px — in Displayee, Localey, Dashee, Assistee, and Replyee, each app having independently reimplemented the same broken shell.
This is the key idea in the whole guide. Once it clicks, the solution is obvious.
CSS has a priority system. When two rules target the same element, the more "specific" one wins. Inline styles sit near the top of that hierarchy — higher than any normal class rule. So this fails:
/* The stylesheet says: */
.dashboard { grid-template-columns: 1fr; }
/* But the element says: */
<div class="dashboard" style="grid-template-columns: 220px 1fr">
/* Result: the inline style wins. Nothing changes. */ ✗
!important!important is an override flag that jumps a stylesheet rule above inline styles. It's the one thing that outranks them:
.dashboard { grid-template-columns: 1fr !important; }
/* Now the stylesheet wins, even against an inline style. */ ✓
In everyday code !important is considered bad practice, because it makes styles hard to reason about. Here it's the correct tool: we are deliberately overriding legacy inline styles we don't want to hand-edit across 200+ call sites.
This is the part that makes it a one-file solution rather than a months-long refactor. CSS can select elements by what's inside their attributes, including the style attribute itself:
/* "Any element whose style attribute contains the text
'grid-template-columns' — whatever its value is" */
[style*="grid-template-columns"] { ... }
Read that carefully. It means we can find every inline grid in the entire app automatically — without knowing where they are, without listing them, without editing a single component file. React renders gridTemplateColumns into the DOM as grid-template-columns, so the match works on all of them.
@media (max-width: 640px) {
.boom-rwd [style*="grid-template-columns"] {
grid-template-columns: 1fr !important;
}
}
In one rule: on screens 640px and under, find every element with an inline grid anywhere inside a .boom-rwd container, and collapse it to a single column — overriding the inline style.
Add class="boom-rwd" to the <body> of an app and every inline grid in that app becomes responsive at once. That is the "one solution they all use."
One stylesheet, copied identically into every app. This is the entire solution.
Each numbered layer below solves one class of defect found in the audit. Layers 1–3 need no per-element work at all.
/* ============================================================
boom-responsive.css — Boom Media shared mobile layer
Add class="boom-rwd" to <body>, then import this file.
============================================================ */
/* --- LAYER 1: iOS auto-zoom -------------------------------
Safari zooms the whole page when you focus an input whose
font-size is under 16px. Fixes checkout + widget + logins. */
@media (max-width: 900px) {
.boom-rwd input,
.boom-rwd select,
.boom-rwd textarea { font-size: 16px !important; }
}
/* --- LAYER 2: tables scroll instead of breaking the page --- */
@media (max-width: 900px) {
.boom-rwd table:not(.boom-keep) {
display: block;
overflow-x: auto;
max-width: 100%;
-webkit-overflow-scrolling: touch;
}
/* parents that clip instead of scroll (Bloggy, Signnee) */
.boom-rwd .overflow-hidden:has(table) { overflow-x: auto !important; }
}
/* --- LAYER 3: collapse every inline grid ------------------- */
@media (max-width: 640px) {
.boom-rwd [style*="grid-template-columns"]:not(.boom-keep) {
grid-template-columns: 1fr !important;
}
}
/* tablet band: 2 columns rather than 1 */
@media (min-width: 641px) and (max-width: 900px) {
.boom-rwd [style*="repeat(4"]:not(.boom-keep),
.boom-rwd [style*="repeat(3"]:not(.boom-keep) {
grid-template-columns: repeat(2, 1fr) !important;
}
}
/* --- LAYER 4: sidebar → drawer (needs 2 class names) ------- */
.boom-menu-btn { display: none; }
@media (max-width: 900px) {
.boom-rwd .boom-sidebar {
position: fixed !important;
top: 0; bottom: 0; left: 0;
z-index: 60;
transform: translateX(-100%);
transition: transform .25s ease;
}
.boom-rwd.boom-open .boom-sidebar { transform: none; }
.boom-rwd .boom-main {
margin-left: 0 !important;
max-width: 100vw !important;
padding: 16px !important;
}
.boom-menu-btn {
display: flex; position: fixed; top: 12px; left: 12px;
z-index: 70; width: 44px; height: 44px;
align-items: center; justify-content: center;
background: #14141c; color: #e8e8f0;
border: 1px solid #2a2a38; border-radius: 10px;
}
}
/* --- LAYER 5: no page-level horizontal scrolling ----------- */
@media (max-width: 900px) {
.boom-rwd { overflow-x: hidden; max-width: 100vw; }
.boom-rwd img,
.boom-rwd video,
.boom-rwd iframe { max-width: 100%; }
.boom-rwd .boom-tap { min-height: 44px; min-width: 44px; }
}
/* --- LAYER 6: notch + iOS viewport height ------------------ */
.boom-rwd .boom-dvh { height: 100dvh; }
.boom-rwd .boom-safe { padding-bottom: env(safe-area-inset-bottom); }
100dvh matters (Layer 6). 100vh on iOS Safari measures the screen including the collapsing address bar, so a full-height panel overflows and its bottom is hidden behind browser chrome. dvh ("dynamic viewport height") tracks the actually-visible area. This is the specific bug on Signnee's signing page — where failure means an unsigned contract.
Five steps. Identical in every app. Roughly ten minutes each.
Drop boom-responsive.css into the app at src/styles/boom-responsive.css. Same file, byte for byte, in every app — that's what makes it maintainable.
In src/app/globals.css, add the import at the bottom of the file. Last import wins ties, and we want this layer to have the final say.
/* globals.css — last line */
@import "../styles/boom-responsive.css";
In src/app/layout.tsx, add boom-rwd to the body tag. Nothing in Layers 1, 2, 3 or 5 activates without it — which also means removing this one word instantly reverts everything.
<body className="boom-rwd">{children}</body>
Layer 4 is the only part needing more than the body class, because CSS can't guess which div is your sidebar. In the dashboard layout, add two class names — keep the existing inline styles, they're overridden automatically on small screens:
<aside className="boom-sidebar" style={{ width: 220 }}>
<main className="boom-main" style={{ marginLeft: 220 }}>
The drawer needs something to open it. This is the only new component — a client component that toggles boom-open on the body:
'use client'
export function BoomMenuButton() {
return (
<button
className="boom-menu-btn"
aria-label="Toggle navigation"
onClick={() => document.body.classList.toggle('boom-open')}
>☰</button>
)
}
Render it once inside the dashboard layout. It's display:none above 900px, so desktop is untouched.
The escape hatch, and the places you'll definitely need it.
Layer 3 collapses every inline grid on phones. Some layouts must not collapse. Add boom-keep and that element is excluded — the :not(.boom-keep) in each rule is what makes this work.
/* Signage output — must stay 40/60 on a TV, never collapse */
<div className="boom-keep" style={{ gridTemplateColumns: '40% 60%' }}>
| App | What | Why |
|---|---|---|
| Displayee | Signage player + video wall | Output renders on TVs. Already correctly built with vw/vh units — do not touch. |
| boo-v2 | POS counter, KDS | Tablet-targeted by design. The cart rail is already fluid. |
| boo-v2 | Order calendar (7-col) | A week view needs 7 columns. Wrap in a scroll container instead. |
| Addee / Bloggy | ContentCalendar (7-col month) | Same reason. Long-term this wants a mobile agenda view, not a grid. |
| Approvee | Device-preview widths | The tool deliberately simulates desktop/tablet widths. |
boom-keep wherever something looks wrong. Trying to predict every exclusion in advance takes longer and you'll still miss some. Let the browser tell you.
Being precise about the boundary, so nothing gets assumed done that isn't.
widget.js, doesn't read the app's stylesheetMobileNav.tsx exists but is imported by nothingwidth: 320) not inside a gridboom-responsive.css. It's also the single highest-mobile-traffic surface in the portfolio, and it currently overflows a 360px screen (width:340px + right:24px = 364px) with a 13px input that triggers iOS zoom. It needs the same rules pasted directly into widget.js's own style block. Treat it as its own task.
| App | Before | After shared CSS | Hand work still needed |
|---|---|---|---|
| Compliee | GOOD | GOOD | None — this app is the reference |
| QRcodee | NEEDS WORK | GOOD | Sidebar drawer classes |
| Signnee | NEEDS WORK | GOOD | boom-dvh on signing iframe |
| Addee | NEEDS WORK | GOOD | ContentCalendar mobile view |
| Bloggy | NEEDS WORK | GOOD | ContentCalendar; approval page check |
| boo-v2 ordering | GOOD | GOOD | Verify 16px didn't shift the layout |
| Rankee | BROKEN | NEEDS WORK | Wire up MobileNav — 1 line |
| Posttee | BROKEN | GOOD | Sidebar classes only |
| Localey | BROKEN | NEEDS WORK | Grid pseudo-tables → cards |
| Dashee | BROKEN | NEEDS WORK | 6-col grid tables; portal nav |
| Displayee | BROKEN | NEEDS WORK | boom-keep on all signage output first |
| Approvee | BROKEN | NEEDS WORK | Review canvas — touch events |
| Assistee | BROKEN | GOOD | Sidebar classes only |
| Replyee dashboard | BROKEN | NEEDS WORK | Inbox → single-pane |
| Replyee widget | BROKEN | BROKEN | Separate fix in widget.js |
Don't trust the desktop browser window. Test the things that actually break.
F12, then Ctrl+Shift+M. Test at 360px (common Android), 375px (iPhone SE), and 768px (tablet). The 641–900px band is where partial fixes tend to hide.document.querySelectorAll('*').forEach(el => {
if (el.scrollWidth > document.body.clientWidth)
console.log('OVERFLOW:', el)
})
/rate. These are surfaces where a customer — or a customer's customer — forms an impression.max-width media query, so it should be. Verify rather than assume.This stylesheet is a retrofit for existing code, not a licence to keep writing inline layout. New work should use Tailwind classes with sm: / md: / lg: prefixes so it's responsive at the source and never needs an !important override.
Rankee and Posttee are the cautionary examples — both were forked from a codebase that already had the inline-style habit, and both inherited the defect wholesale. Fixing the pattern now stops the next fork from inheriting it too.