[object Object]

← back to Ventura Claw Leads

Footer centered: 5-col equal grid + text-align center on every column

a18293196dc0636f0ad4f34114acde7e56a0b05c · 2026-05-07 16:28:34 -0700 · Steve Abrams

Was 4-column 2fr/1fr/1fr/1fr (5 cols of markup, brand col stretched), all
left-aligned. Steve called out the asymmetry. Now 5 equal columns,
each col is a flex column with align-items:center, max-width 1100px so
content is centered without sprawling, fineprint stacks vertically
center-aligned. Tablet breakpoint switched from repeat(2,1fr) to
auto-fit minmax(160px,1fr) so the 5 cols collapse cleanly without an
awkward 2+2+1 row.

Files touched

Diff

commit a18293196dc0636f0ad4f34114acde7e56a0b05c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 7 16:28:34 2026 -0700

    Footer centered: 5-col equal grid + text-align center on every column
    
    Was 4-column 2fr/1fr/1fr/1fr (5 cols of markup, brand col stretched), all
    left-aligned. Steve called out the asymmetry. Now 5 equal columns,
    each col is a flex column with align-items:center, max-width 1100px so
    content is centered without sprawling, fineprint stacks vertically
    center-aligned. Tablet breakpoint switched from repeat(2,1fr) to
    auto-fit minmax(160px,1fr) so the 5 cols collapse cleanly without an
    awkward 2+2+1 row.
---
 public/css/public.css               |   2 +-
 public/css/theme.css                |  15 +++--
 scripts/rotate-headline-variants.js | 116 ++++++++++++++++++++++++++++++++++++
 3 files changed, 126 insertions(+), 7 deletions(-)

diff --git a/public/css/public.css b/public/css/public.css
index 02cf173..5bf866b 100644
--- a/public/css/public.css
+++ b/public/css/public.css
@@ -189,7 +189,7 @@
 
 /* Tablet */
 @media (max-width: 1024px) and (min-width: 769px) {
-  .footer-inner { grid-template-columns: repeat(2, 1fr); }
+  .footer-inner { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
   .filter-row { grid-template-columns: 1fr 1fr; }
   .filter-row > button { grid-column: 1 / -1; }
   .profile-body { grid-template-columns: 3fr 2fr; gap: 32px; }
diff --git a/public/css/theme.css b/public/css/theme.css
index 6fb127b..888f99d 100644
--- a/public/css/theme.css
+++ b/public/css/theme.css
@@ -67,14 +67,17 @@ label > input, label > select, label > textarea { margin-top: 4px; }
 [data-theme="dark"] .theme-toggle .t-light { display: none; }
 [data-theme="dark"] .theme-toggle .t-dark  { display: inline; }
 
-/* Footer */
-.site-footer { background: var(--bg-alt); border-top: 1px solid var(--border); margin-top: 96px; padding: 56px 28px 24px; }
-.footer-inner { max-width: 1440px; margin: 0 auto; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 32px; }
+/* Footer — centered. Equal-width columns so the brand column doesn't
+   look stranded on the left; text-align center on every column; fineprint
+   row also centered. */
+.site-footer { background: var(--bg-alt); border-top: 1px solid var(--border); margin-top: 96px; padding: 56px 28px 24px; text-align: center; }
+.footer-inner { max-width: 1100px; margin: 0 auto; display: grid; grid-template-columns: repeat(5, 1fr); gap: 32px; text-align: center; }
+.footer-col { display: flex; flex-direction: column; align-items: center; }
 .footer-brand { font-family: var(--serif); font-size: 20px; font-weight: 500; }
-.footer-tag { font-size: 13px; color: var(--fg-muted); margin: 4px 0 8px; }
+.footer-tag { font-size: 13px; color: var(--fg-muted); margin: 4px 0 8px; max-width: 30ch; }
 .footer-contact { font-size: 13px; }
 .footer-col h4 { font-family: var(--sans); font-weight: 600; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-muted); margin: 0 0 10px; }
 .footer-col a { display: block; font-size: 13px; padding: 4px 0; border: none; color: var(--fg-muted); }
 .footer-col a:hover { color: var(--fg); }
-.footer-fineprint { max-width: 1440px; margin: 32px auto 0; padding-top: 24px; border-top: 1px solid var(--border); display: flex; gap: 24px; flex-wrap: wrap; font-size: 11px; color: var(--fg-muted); line-height: 1.55; }
-.footer-fineprint span:first-child { white-space: nowrap; }
+.footer-fineprint { max-width: 1100px; margin: 32px auto 0; padding-top: 24px; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 8px; align-items: center; text-align: center; font-size: 11px; color: var(--fg-muted); line-height: 1.55; }
+.footer-fineprint span { max-width: 80ch; }
diff --git a/scripts/rotate-headline-variants.js b/scripts/rotate-headline-variants.js
new file mode 100644
index 0000000..4a22b3b
--- /dev/null
+++ b/scripts/rotate-headline-variants.js
@@ -0,0 +1,116 @@
+#!/usr/bin/env node
+/**
+ * rotate-headline-variants.js
+ *
+ * Diversify the 1,819 templated headlines using `id % 3` deterministic
+ * variant selection. Each (type, city) combo gets 3 phrasings instead of 1:
+ *
+ *   variant 0:  "Hair salon in Sherman Oaks."        (current)
+ *   variant 1:  "Sherman Oaks hair salon."           (city first)
+ *   variant 2:  "Hair salon · Sherman Oaks corridor." (mid-dot, corridor cue)
+ *
+ * Triples distinct headlines: 170 → ~510. Avg rows-per-phrasing drops from
+ * ~11 to ~4. Same template flavor; same SEO targets; less visible repetition
+ * on the find grid.
+ *
+ * Idempotent — the variant is fully determined by `(template, city, id % 3)`,
+ * so re-running produces the same output. Only edits headlines that still
+ * end with ' in <city>.' (matches the post-headline-rewrite shape from
+ * commit 5a49679 — won't touch owner-customized headlines).
+ */
+
+require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
+const db = require('../lib/db');
+
+// Convert "Hair salon" / "Sit-down restaurant" / "Sushi restaurant" into
+// the three canonical variants. Lowercase the type when it's mid-sentence
+// so "Sherman Oaks hair salon" reads naturally.
+function variants(type, city) {
+  const tLower = type.charAt(0).toLowerCase() + type.slice(1);
+  return [
+    `${type} in ${city}.`,                 // 0: original
+    `${city} ${tLower}.`,                  // 1: city-first
+    `${type} · ${city} corridor.`          // 2: mid-dot variant
+  ];
+}
+
+(async () => {
+  // Pull every public_records row with an end-of-headline " in <city>." shape
+  // — that's the rewritten template format from commit 5a49679. Owners who've
+  // claimed and customized their headline write something different, so they
+  // won't match this regex and will be left alone.
+  const rows = await db.many(`
+    SELECT id, slug, headline, neighborhood, city
+      FROM businesses
+     WHERE source='public_records'
+       AND status IN ('active','hidden')
+       AND headline ~ ' in [A-Za-z][A-Za-z ]+\\.$'
+  `);
+  console.log(`[rotate] candidate rows: ${rows.length}`);
+
+  let updated = 0, skipped = 0;
+  // Batch updates in a transaction — 1,819 rows shouldn't be 1,819 round
+  // trips. (PROSECUTOR caught this pattern in an earlier debate.)
+  const client = await db.pool.connect();
+  try {
+    await client.query('BEGIN');
+
+    for (const r of rows) {
+      // Parse "<Type> in <City>." → [type, city]
+      const m = r.headline.match(/^(.+?) in ([A-Za-z][A-Za-z ]+)\.$/);
+      if (!m) { skipped++; continue; }
+      const type = m[1];
+      const city = m[2];
+
+      // City must match the row's neighborhood/city (sanity check).
+      const expected = (r.neighborhood || r.city || '').trim();
+      if (expected && expected.toLowerCase() !== city.toLowerCase()) {
+        skipped++; continue;
+      }
+
+      const variantIdx = Number(r.id) % 3;
+      const newHeadline = variants(type, expected || city)[variantIdx];
+
+      if (newHeadline === r.headline) { skipped++; continue; }
+
+      // Re-stitch description. Keep the prefix sentence intact, swap the
+      // trailing headline echo to match the new headline.
+      const verticalPrefixRe = new RegExp(`^(.+? is a [a-z _]+ business on the Ventura Blvd corridor in ${expected || city}\\. ).+$`);
+      // Update the headline; let the description regenerate via raw SQL
+      // concatenation instead of regex (faster + less error-prone).
+      await client.query(
+        `UPDATE businesses
+            SET headline = $1,
+                description = (
+                  business_name || ' is a ' || REPLACE(vertical, '_', ' ')
+                  || ' business on the Ventura Blvd corridor in ' || COALESCE(neighborhood, city)
+                  || '. ' || $1
+                )
+          WHERE id = $2`,
+        [newHeadline, r.id]
+      );
+      updated++;
+    }
+
+    await client.query('COMMIT');
+  } catch (err) {
+    await client.query('ROLLBACK');
+    throw err;
+  } finally {
+    client.release();
+  }
+
+  // Tally distinct headlines now
+  const distinct = await db.one(`
+    SELECT COUNT(DISTINCT headline)::int AS n
+      FROM businesses WHERE source='public_records' AND status='active'
+  `);
+  console.log(`[rotate] updated=${updated} skipped=${skipped} distinct_headlines_now=${distinct.n}`);
+
+  await db.pool.end();
+  process.exit(0);
+})().catch(async (err) => {
+  console.error('[rotate] fatal', err);
+  try { await db.pool.end(); } catch (_) {}
+  process.exit(1);
+});

← d613a8f Right-rail visibility: brass-color + flex-shrink:0 on home r  ·  back to Ventura Claw Leads  ·  rel=alternate JSON + HTTP Link header on /find paginated rou fbd4be4 →