openvibers/openvibe.rent Commits
City tables name their columns on a phone (#178)
Changes
@@@ -93,19 +93,20 @@ button.sc-btn.sc-primary { border: 0; }.ew-chips { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; margin: 12px 0 6px; }.ew-chips li { padding: 5px 11px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); font-size: .9rem; }--/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more shows each row as a card, the first++/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more whose cells name their column++ (data-label) shows each row as a card, the firstcell its title and every other cell named by its column (html.js gives each cell its data-label). Narrower tablesstay tables. Nothing is left out and nothing scrolls sideways. */@media (max-width: 640px) {-- .table-wrap:has(> table thead th:nth-child(3)) { overflow: visible; }-- .table-wrap > table:has(thead th:nth-child(3)) { display: block; min-width: 0; }-- .table-wrap > table:has(thead th:nth-child(3)) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }-- .table-wrap > table:has(thead th:nth-child(3)) tbody { display: grid; gap: 10px; }-- .table-wrap > table:has(thead th:nth-child(3)) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }-- .table-wrap > table:has(thead th:nth-child(3)) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }-- .table-wrap > table:has(thead th:nth-child(3)) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }-- .table-wrap > table:has(thead th:nth-child(3)) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }-- .table-wrap > table:has(thead th:nth-child(3)) td:empty { display: none; }++ .table-wrap:has(> table thead th:nth-child(3)):has(> table td[data-label]) { overflow: visible; }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) { display: block; min-width: 0; }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tbody { display: grid; gap: 10px; }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td:empty { display: none; }}/* A sign-in prompt: the sentence, then the button under it (2026-11-01). */.notice.signin-prompt { display: grid; gap: 12px; justify-items: start; }@@@ -113,3 +114,10 @@ button.sc-btn.sc-primary { border: 0; }/* The rent trend line is drawn 320 wide; on a 320 px phone its box overran the gutter (2026-11-01). */svg.rent-trend { max-width: 100%; height: auto; }++@media (max-width: 640px) {++ /* A row's own header (th scope=row) is the card's title, like a first cell. */++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tbody th { display: block; padding: 0; border: 0; text-align: left; font-weight: 700; }++ /* The city tables are all figures: two to a row, the city across the top. */++ .table-wrap > table.city-table:has(td[data-label]) tr { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }++ .table-wrap > table.city-table:has(td[data-label]) tbody th { grid-column: 1 / -1; }++}@@@ -52,7 +52,7 @@ function mountHomesCityPages(r, { config, s, page }) {<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2><div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical 2-bedroom rent in the 30 largest US cities</caption><thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>--<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}, ${c.state}</a></th><td>${valueCell(c)}</td><td>${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>++<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}, ${c.state}</a></th><td data-label="Median home value">${valueCell(c)}</td><td data-label="2-bedroom rent">${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>${source()}</section>`,});});@@@ -72,7 +72,7 @@ ${source()}</section>`,<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what homes are worth, what renting costs, and a page for each with every for-sale site's search.</p><div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical rent by city in ${name}</caption><thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>--<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td>${valueCell(c)}</td><td>${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>++<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td data-label="Median home value">${valueCell(c)}</td><td data-label="2-bedroom rent">${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>${source()}<p><a href="/in">Every state</a> · <a href="/records">Property records</a> · <a href="/mortgage">Mortgage calculator</a></p>`,});@@@ -102,7 +102,7 @@ ${city ? html`<p class="small muted fair-how">Within the Census' margin of the m<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2><div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents in the 30 largest US cities</caption><thead><tr><th scope="col">City</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>--<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${placeLabel(c)}</a></th><td>${cell(c, 'b1')}</td><td>${cell(c, 'b2')}</td><td>${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>++<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${placeLabel(c)}</a></th><td data-label="1 bedroom">${cell(c, 'b1')}</td><td data-label="2 bedrooms">${cell(c, 'b2')}</td><td data-label="3 bedrooms">${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>${source()}</section>${cpiBlock({ national, metro: null })}`,});});@@@ -123,7 +123,7 @@ ${source()}</section>${cpiBlock({ national, metro: null })}`,<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what rent usually costs, and a page for each with every rental site's search for it.</p><div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents by city in ${name}</caption><thead><tr><th scope="col">City</th><th scope="col">Studio</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>--<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td>${cell(c, 'studio')}</td><td>${cell(c, 'b1')}</td><td>${cell(c, 'b2')}</td><td>${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>++<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td data-label="Studio">${cell(c, 'studio')}</td><td data-label="1 bedroom">${cell(c, 'b1')}</td><td data-label="2 bedrooms">${cell(c, 'b2')}</td><td data-label="3 bedrooms">${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>${source()}<p><a href="/in">Every state</a> · <a href="/fair">Check a rent</a></p>`,});
@@ -93,19 +93,20 @@ button.sc-btn.sc-primary { border: 0; }
@@ -93,19 +93,20 @@ button.sc-btn.sc-primary { border: 0; }
.ew-chips { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; margin: 12px 0 6px; }
.ew-chips { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; margin: 12px 0 6px; }
.ew-chips li { padding: 5px 11px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); font-size: .9rem; }
.ew-chips li { padding: 5px 11px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); font-size: .9rem; }
-/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more shows each row as a card, the first
+/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more whose cells name their column
+ (data-label) shows each row as a card, the first
cell its title and every other cell named by its column (html.js gives each cell its data-label). Narrower tables
cell its title and every other cell named by its column (html.js gives each cell its data-label). Narrower tables
stay tables. Nothing is left out and nothing scrolls sideways. */
stay tables. Nothing is left out and nothing scrolls sideways. */
@media (max-width: 640px) {
@media (max-width: 640px) {
- .table-wrap:has(> table thead th:nth-child(3)) { overflow: visible; }
- .table-wrap > table:has(thead th:nth-child(3)) { display: block; min-width: 0; }
- .table-wrap > table:has(thead th:nth-child(3)) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
- .table-wrap > table:has(thead th:nth-child(3)) tbody { display: grid; gap: 10px; }
- .table-wrap > table:has(thead th:nth-child(3)) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }
- .table-wrap > table:has(thead th:nth-child(3)) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }
- .table-wrap > table:has(thead th:nth-child(3)) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }
- .table-wrap > table:has(thead th:nth-child(3)) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
- .table-wrap > table:has(thead th:nth-child(3)) td:empty { display: none; }
+ .table-wrap:has(> table thead th:nth-child(3)):has(> table td[data-label]) { overflow: visible; }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) { display: block; min-width: 0; }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tbody { display: grid; gap: 10px; }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td:empty { display: none; }
}
}
/* A sign-in prompt: the sentence, then the button under it (2026-11-01). */
/* A sign-in prompt: the sentence, then the button under it (2026-11-01). */
.notice.signin-prompt { display: grid; gap: 12px; justify-items: start; }
.notice.signin-prompt { display: grid; gap: 12px; justify-items: start; }
@@ -113,3 +114,10 @@ button.sc-btn.sc-primary { border: 0; }
@@ -113,3 +114,10 @@ button.sc-btn.sc-primary { border: 0; }
/* The rent trend line is drawn 320 wide; on a 320 px phone its box overran the gutter (2026-11-01). */
/* The rent trend line is drawn 320 wide; on a 320 px phone its box overran the gutter (2026-11-01). */
svg.rent-trend { max-width: 100%; height: auto; }
svg.rent-trend { max-width: 100%; height: auto; }
+@media (max-width: 640px) {
+ /* A row's own header (th scope=row) is the card's title, like a first cell. */
+ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tbody th { display: block; padding: 0; border: 0; text-align: left; font-weight: 700; }
+ /* The city tables are all figures: two to a row, the city across the top. */
+ .table-wrap > table.city-table:has(td[data-label]) tr { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
+ .table-wrap > table.city-table:has(td[data-label]) tbody th { grid-column: 1 / -1; }
+}
@@ -52,7 +52,7 @@ function mountHomesCityPages(r, { config, s, page }) {
@@ -52,7 +52,7 @@ function mountHomesCityPages(r, { config, s, page }) {
<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2>
<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical 2-bedroom rent in the 30 largest US cities</caption>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical 2-bedroom rent in the 30 largest US cities</caption>
<thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>
<thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>
-<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}, ${c.state}</a></th><td>${valueCell(c)}</td><td>${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
+<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}, ${c.state}</a></th><td data-label="Median home value">${valueCell(c)}</td><td data-label="2-bedroom rent">${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
${source()}</section>`,
${source()}</section>`,
});
});
});
});
@@ -72,7 +72,7 @@ ${source()}</section>`,
@@ -72,7 +72,7 @@ ${source()}</section>`,
<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what homes are worth, what renting costs, and a page for each with every for-sale site's search.</p>
<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what homes are worth, what renting costs, and a page for each with every for-sale site's search.</p>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical rent by city in ${name}</caption>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical rent by city in ${name}</caption>
<thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>
<thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>
-<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td>${valueCell(c)}</td><td>${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
+<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td data-label="Median home value">${valueCell(c)}</td><td data-label="2-bedroom rent">${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
${source()}
${source()}
<p><a href="/in">Every state</a> · <a href="/records">Property records</a> · <a href="/mortgage">Mortgage calculator</a></p>`,
<p><a href="/in">Every state</a> · <a href="/records">Property records</a> · <a href="/mortgage">Mortgage calculator</a></p>`,
});
});
@@ -102,7 +102,7 @@ ${city ? html`<p class="small muted fair-how">Within the Census' margin of the m
@@ -102,7 +102,7 @@ ${city ? html`<p class="small muted fair-how">Within the Census' margin of the m
<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2>
<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents in the 30 largest US cities</caption>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents in the 30 largest US cities</caption>
<thead><tr><th scope="col">City</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>
<thead><tr><th scope="col">City</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>
-<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${placeLabel(c)}</a></th><td>${cell(c, 'b1')}</td><td>${cell(c, 'b2')}</td><td>${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
+<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${placeLabel(c)}</a></th><td data-label="1 bedroom">${cell(c, 'b1')}</td><td data-label="2 bedrooms">${cell(c, 'b2')}</td><td data-label="3 bedrooms">${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
${source()}</section>${cpiBlock({ national, metro: null })}`,
${source()}</section>${cpiBlock({ national, metro: null })}`,
});
});
});
});
@@ -123,7 +123,7 @@ ${source()}</section>${cpiBlock({ national, metro: null })}`,
@@ -123,7 +123,7 @@ ${source()}</section>${cpiBlock({ national, metro: null })}`,
<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what rent usually costs, and a page for each with every rental site's search for it.</p>
<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what rent usually costs, and a page for each with every rental site's search for it.</p>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents by city in ${name}</caption>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents by city in ${name}</caption>
<thead><tr><th scope="col">City</th><th scope="col">Studio</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>
<thead><tr><th scope="col">City</th><th scope="col">Studio</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>
-<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td>${cell(c, 'studio')}</td><td>${cell(c, 'b1')}</td><td>${cell(c, 'b2')}</td><td>${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
+<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td data-label="Studio">${cell(c, 'studio')}</td><td data-label="1 bedroom">${cell(c, 'b1')}</td><td data-label="2 bedrooms">${cell(c, 'b2')}</td><td data-label="3 bedrooms">${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
${source()}
${source()}
<p><a href="/in">Every state</a> · <a href="/fair">Check a rent</a></p>`,
<p><a href="/in">Every state</a> · <a href="/fair">Check a rent</a></p>`,
});
});