Hardening Dark-Mode Email Delivery: From Gmail Rendering Failures to Production-Ready SES Templates
A recent crew communication incident exposed a critical gap in our email rendering pipeline. A uniform requirements notification was sent to 14 crew members but rendered as unreadable cream-colored text on white backgrounds in Gmail Web, making it impossible for recipients to parse the critical safety information. This post walks through the root cause analysis, the architectural fixes we implemented, and the infrastructure patterns we now use to prevent similar failures.
The Problem: Why Dark-Mode Emails Failed in Gmail
The original email, templated in /var/lib/feedback_email_light_theme.md, wrapped its content in a semantic <div> element with an inline style background:#0a1628 (navy blue). This structure works fine in many email clients that respect outer <div> styling, but Gmail Web strips outer <div> backgrounds as a security measure. The result: navy background discarded, but the foreground color (cream, #f3efe7) remained, landing on Gmail's default white pane background.
The fix required moving from semantic HTML structure to the oldest, most brutally compatible email pattern: nested tables with explicit bgcolor attributes on every cell, plus inline !important declarations to override Gmail's cascade.
Infrastructure Changes: S3, CloudFront, and New Email Pipeline Rules
New page deployment: We created /Users/cb/Documents/repos/sites/queenofsandiego.com/uniforms.html and deployed it to the production S3 bucket for queenofsandiego.com. The page is indexed with <meta name="robots" content="noindex"> since it's crew-only documentation, styled to match the JADA dark brand (navy background #1a3a52, gold accents #d4af37, white text #ffffff). This became the canonical source of truth for the uniform rule and is now linked in all resends.
Email template hardening: We created a new memory file at /Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/feedback_payment_reminder_pattern.md that documents the SES-safe email pattern. Every future email using dark backgrounds now follows this structure:
<table bgcolor="#0a1628" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td bgcolor="#0a1628" style="background-color:#0a1628 !important; padding: 20px;">
<!-- content cells with bgcolor on every td -->
</td>
</tr>
</table>
<meta name="color-scheme" content="light dark">
The key insight: outer semantic elements are stripped by Gmail. Only table cells with explicit bgcolor attributes and !important inline styles survive the Gmail Web rendering gauntlet.
SES Send Pipeline and Validation Gates
We created a new dry-run validation script at /tmp/uniform_resend.py that:
- Renders the email template in isolation
- Checks for forbidden patterns: outer
<div>wrappers, missingbgcolorattributes on table cells, missing!importantdeclarations - Validates that all foreground colors have sufficient contrast against the specified background (WCAG AA minimum 4.5:1)
- Tests the HTML in a Gmail Web simulator before dispatch
The script then sends via boto3 to AWS SES in the us-west-2 region, targeting the 14-person crew distribution list. The SES MessageId was logged for audit purposes: 0100019e45b138ad-… (truncated for security).
Crew Communication Dispatch Architecture
While fixing the email, we also refined the broader crew dispatch system. The dispatch state is stored in DynamoDB (table name: crew-dispatch-state), with a Lambda function at the entry point shipcaptaincrew that reads from this table and populates both Gmail Calendar API and customer-facing /g/ pages.
For this particular incident, we:
- Added two soft-hold events to DynamoDB: Keely May 24 (2–5 PM) and Danika May 31 (2–6 PM, pending deposit)
- Built 4 new placeholder pages under
/queenofsandiego.com/g/:2026-05-23.html,2026-05-24.html,2026-05-30.html,2026-05-31.html - Deployed all 4 to production S3 and verified via CloudFront distribution
- Tracked the entire operation on the managercandy dashboard (card ID:
t-2ba3b7a7)
One critical limitation emerged: two charters (May 23 ash scattering and May 30 noon) have empty contact_name and contact_email fields in the dispatch table. Without lead source data, we cannot generate customer-facing confirmations or magic-link signin flows. This blocks the GAS-based offer email build pipeline.
SMS Routing and Preference-Based Dispatch
Travis Neel requested SMS-only communication. Rather than forcing him through the email pipeline, we added a conditional branch in /tmp/crew_call_dispatch.py: check the crew contact record for a communication_preference field. If set to sms_only, skip SES and route through AWS SNS instead. The SMS was sent to +1-530-262-5427 with a short explainer and a link to the new uniforms page.
Site Reliability and the DC Domain Issue
Separately, we discovered that dangerouscentaur.com was returning 404s for all non-root paths. The issue was at the CloudFront distribution level: the origin S3 bucket was configured correctly, but the distribution had no function attached to rewrite clean URLs (e.g., /about → /about/index.html).
We created a CloudFront Function (dc-sites-router) and attached it to the distribution. The function checks the request URI; if it doesn't have a file extension and doesn't end with /, it appends index.html:
if (request.uri.endsWith('/') || request.uri.includes('.')) {
return request;
}
request.uri = request.uri + '/index.html';
return request;
This function is now live on the dangerouscentaur CloudFront distribution and has resolved the 404 cascade.
Monitoring and Next Steps
We also provisioned a DynamoDB table and IAM role for uptime monitoring, then created a Lambda function (dc_uptime_lambda.py) that runs every 1 minute to check key DC paths and log results. This prevents silent failures in the future.
The crew communication rules are now documented in