Email Rendering in SES: Why Your Dark Theme Broke in Gmail and How We Fixed It
Last week, a crew uniform policy email landed in 14 inboxes completely unreadable—cream-colored text on a white background. The charter captain wasn't amused. This incident exposed a critical gap in our email rendering pipeline: how Gmail Web strips outer container styles and why table-based layouts with explicit cell styling are non-negotiable for SES sends.
Here's what went wrong, how we debugged it, and the architectural fix we deployed.
The Root Cause: CSS Containment Failure in Gmail
The original email template (rendered via /tmp/uniform_resend.py) wrapped all content in a div with inline background styling:
<div style="background:#0a1628; color:#f3efe7;">
<!-- email content -->
</div>
This works fine in desktop clients like Outlook or Apple Mail. Gmail Web Client, however, strips outer <div> backgrounds as a security and rendering optimization. The email renderer saw:
- No background color (stripped)
- Text color:
#f3efe7(cream, preserved) - Default Gmail Web background: white
- Result: cream text on white = invisible
The preview file (/Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/feedback_email_light_theme.md) had been tested in a limited desktop context, not against the Gmail Web rendering engine where most crew members open email on mobile.
The Fix: Table-Based Structure with Explicit Cell Styling
We rebuilt the email template using a table-wrapped structure with bgcolor attributes on every table cell and inline !important flags. Here's the pattern:
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#0a1628">
<tr>
<td bgcolor="#0a1628" style="color:#f3efe7 !important; padding:20px;">
<h2 style="color:#d4af37 !important;">JADA Uniform Policy</h2>
<p>Your email content here</p>
</td>
</tr>
</table>
Key decisions:
- Table as root container: Gmail respects table
bgcolorattributes where it strips div backgrounds - Per-cell bgcolor: Even nested cells get explicit background colors to prevent color leakage
- !important flags: Override any inherited or Gmail-injected styles
- color-scheme meta tag: Added to HTML head to hint to dark-mode clients
- No nested divs: Banned from SES sends going forward
The resend script (/tmp/uniform_resend.py) implemented this hardened template and sent via AWS SES with MessageId 0100019e45b138ad-… to all 14 crew members. The new email rendered correctly across Gmail Web, Gmail mobile, Outlook, and Apple Mail.
Supporting Infrastructure: The Uniforms Page
The email included a link to a new policy page that didn't exist: https://queenofsandiego.com/uniforms.html. We built and deployed this as a static HTML file:
File location: /Users/cb/Documents/repos/sites/queenofsandiego.com/uniforms.html
Deployment:
- S3 bucket:
queenofsandiego.com(primary production bucket) - CloudFront distribution: Standard QOS CDN (cached, 86400s TTL)
- Meta tags:
<meta name="robots" content="noindex">(internal policy doc, not for search ranking) - Design: Navy/gold color scheme matching JADA brand guidelines
- Content structure: Two-card layout (Ash Scattering vs. Standard Charter uniforms)
The page was verified live at https://queenofsandiego.com/uniforms.html and logged as a tracking card on the managercandy dashboard.
Process Documentation: Email Rendering Checklist
To prevent this from happening again, we updated /Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/feedback_email_light_theme.md with a hardened email template and added a critical rule to MEMORY.md:
Rule (now enforced): All SES sends must pass a Gmail Web pane readability gate before dispatch. The gate checks:
- No outer
<div>wrappers with background colors - Root container is
<table>with explicitbgcolor - All text-bearing cells have explicit color +
!important - Contrast ratio ≥ 4.5:1 (WCAG AA minimum)
This checklist is now part of the pre-send audit workflow in /tmp/audit_email_to_cb.py.
Secondary Workflow: SMS Override for Non-Email Preferences
Travis Neel (crew contact) has explicitly opted out of email. Instead of sending him the uniform email, we dispatched an SMS to 530-262-5427 with the policy rule and a link to /uniforms.html. This required a contact preference check in the dispatch logic and separate SMS send path.
Key Architectural Decisions
- Why tables over semantic HTML? Email clients (especially Gmail Web) have inconsistent CSS support. Tables are the lowest common denominator for guaranteed rendering.
- Why bgcolor on every cell? Prevents color bleed in legacy email clients and mobile Gmail where CSS is stripped or reinterpreted.
- Why !important? Gmail sometimes injects inline styles into parsed HTML. !important ensures our colors win.
- Why the noindex meta tag? This is an internal policy document, not a marketing page. We don't want search engines crawling it or it showing up in results.
What's Next
Going forward:
- All email templates in the codebase will be audited against the hardened pattern
- The Gmail Web readability gate will be automated into the SES send pipeline (checking template structure before MessageId is returned)
- Contact preference data (email vs. SMS) is now enforced at dispatch time, not at send time
- A/B rendering tests should happen in a staging environment that mimics Gmail Web's CSS stripping behavior
The uniform email is now readable. The system is more robust. Ship on.