Engineering Guide

Fixing Mobile Across All 14 Apps

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 Media SaaS · Created 2026-07-20 · eric@boommedia.us

What this guide covers

  1. The one root cause — inline styles vs. CSS
  2. Why a normal stylesheet can't fix it (and the trick that can)
  3. The shared file: boom-responsive.css
  4. How to install it in one app — step by step
  5. How to opt an element out when it shouldn't collapse
  6. What this fixes, what it doesn't, and what's left per app
  7. How to actually test it

01The one root cause

Every app rated "BROKEN ON MOBILE" shares a single technical trait. Every app rated GOOD avoids it.

Inline styles cannot contain a breakpoint

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.

The two ways to style

✗ Inline style object

<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.

✓ CSS class (Tailwind)

<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.

Why this matters so much

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.

The pattern held across all 15 codebases with zero exceptions. Inline-styled apps (Rankee, Posttee, Localey, Dashee, Displayee dashboard, Approvee, Assistee, Replyee) were all rated BROKEN. Tailwind apps (Compliee, Signnee, QRcodee, Addee, Bloggy, boo-v2 ordering) were rated GOOD or NEEDS WORK. That is not a coincidence — it is cause and effect.

What it looks like to a real user

Take Posttee's dashboard shell, which is gridTemplateColumns: '220px 1fr' written inline:

SIDEBAR
220px
(fixed)
content
~155px
Today — 375px phone
full-width content

☰ menu opens
sidebar as a drawer
After the fix

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.

02Why a normal stylesheet can't fix it — and the trick that can

This is the key idea in the whole guide. Once it clicks, the solution is obvious.

The specificity problem

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. */ 

Lever one — !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.

Lever two — the attribute selector

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.

Putting the two levers together

@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."

The honest trade-off. This is a blunt instrument by design. It will also collapse grids that should stay multi-column — a digital-signage layout, a POS terminal on a tablet, a 7-day calendar. That's expected and handled: section 05 covers the one-word opt-out. The bet is that fixing ~200 broken grids automatically and re-excluding ~10 is far cheaper than editing 200 by hand.

03The shared file

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); }
Why 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.

04Installing it in one app

Five steps. Identical in every app. Roughly ten minutes each.

1

Copy the file in

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.

2

Import it globally

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";
3

Turn it on with one class

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>
4

Tag the shell for the drawer

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 }}>
5

Add the hamburger

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.

Steps 1–3 alone fix the majority of the damage. If you want to move fast, do those three in every app first — they need no judgment calls and no component knowledge. Steps 4–5 are only needed in apps with a fixed sidebar.

05Opting out — when a grid should NOT collapse

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%' }}>

Known places to apply it

AppWhatWhy
DisplayeeSignage player + video wallOutput renders on TVs. Already correctly built with vw/vh units — do not touch.
boo-v2POS counter, KDSTablet-targeted by design. The cart rail is already fluid.
boo-v2Order calendar (7-col)A week view needs 7 columns. Wrap in a scroll container instead.
Addee / BloggyContentCalendar (7-col month)Same reason. Long-term this wants a mobile agenda view, not a grid.
ApproveeDevice-preview widthsThe tool deliberately simulates desktop/tablet widths.
Work in this order: apply the CSS first, then open each app on a phone and add 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.

06What this fixes — and what it doesn't

Being precise about the boundary, so nothing gets assumed done that isn't.

✓ Fixed automatically

  • iOS zoom on every input — including BOO checkout + Stripe card field
  • ~200 inline grids collapsing to 1 column
  • Tables overflowing or being clipped
  • Fixed sidebars → drawers (5 apps)
  • Page-level horizontal scroll
  • Oversized images / iframes

✗ Still needs hand work

  • Replyee widget — self-injected CSS in widget.js, doesn't read the app's stylesheet
  • Approvee review canvas — needs touch-scroll events, not just CSS
  • Rankee navMobileNav.tsx exists but is imported by nothing
  • Fixed-pixel widths (width: 320) not inside a grid
  • Tap targets under 44px
  • 7-column calendars needing a real mobile view
The Replyee widget is the important exception. It's a standalone script injected into client websites — it builds its own CSS internally and never sees boom-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.

Per-app status after applying the shared layer

AppBeforeAfter shared CSSHand work still needed
ComplieeGOODGOODNone — this app is the reference
QRcodeeNEEDS WORKGOODSidebar drawer classes
SignneeNEEDS WORKGOODboom-dvh on signing iframe
AddeeNEEDS WORKGOODContentCalendar mobile view
BloggyNEEDS WORKGOODContentCalendar; approval page check
boo-v2 orderingGOODGOODVerify 16px didn't shift the layout
RankeeBROKENNEEDS WORKWire up MobileNav — 1 line
PostteeBROKENGOODSidebar classes only
LocaleyBROKENNEEDS WORKGrid pseudo-tables → cards
DasheeBROKENNEEDS WORK6-col grid tables; portal nav
DisplayeeBROKENNEEDS WORKboom-keep on all signage output first
ApproveeBROKENNEEDS WORKReview canvas — touch events
AssisteeBROKENGOODSidebar classes only
Replyee dashboardBROKENNEEDS WORKInbox → single-pane
Replyee widgetBROKENBROKENSeparate fix in widget.js

07How to test it

Don't trust the desktop browser window. Test the things that actually break.

  1. Chrome DevTools device modeF12, 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.
  2. Check for horizontal scroll — the fastest tell. Paste in the console:
    document.querySelectorAll('*').forEach(el => {
      if (el.scrollWidth > document.body.clientWidth)
        console.log('OVERFLOW:', el)
    })
  3. Test on a real iPhone for the input-zoom fix. Simulators do not reproduce Safari's zoom behavior — you have to tap a real field on a real device.
  4. Walk the money paths before anything else: BOO checkout, Signnee's signing page, Approvee's review link, Rewardee's /rate. These are surfaces where a customer — or a customer's customer — forms an impression.
  5. Confirm desktop is unchanged. Every rule is inside a max-width media query, so it should be. Verify rather than assume.

The rule going forward

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.