diff --git a/nextjs-app/__tests__/components/PlaceView.test.tsx b/nextjs-app/__tests__/components/PlaceView.test.tsx
index 931f2db..aeec09e 100644
--- a/nextjs-app/__tests__/components/PlaceView.test.tsx
+++ b/nextjs-app/__tests__/components/PlaceView.test.tsx
@@ -168,3 +168,34 @@ describe('PlaceView presentation', () => {
.not.toBeInTheDocument();
});
});
+
+describe('PlaceView table alignment', () => {
+ const aligned: PlaceDetail = {
+ place: { kind: 'town', slug: 'brentwood', name: 'Brentwood', count: 2,
+ parent_authority: 'Essex', phases: ['primary'] },
+ schools: [
+ { urn: 1, school_name: 'Alpha Primary', phase: 'Primary',
+ rwm_expected_pct: 82, attainment_8_score: null } as never,
+ ],
+ averages: { rwm_expected_pct: 63, attainment_8_score: null },
+ };
+
+ it('aligns the measure heading and its values with the same class', () => {
+ // They were aligned by two different selectors whose specificity did not
+ // match: `.table th:last-child` (0,2,1) won and went right, while `.num`
+ // (0,1,0) lost to `.table td` (0,1,1) and stayed left. Sharing one class
+ // is what makes them impossible to drift apart.
+ const { container } = render();
+ const th = container.querySelectorAll('th')[1];
+ const td = container.querySelectorAll('tbody td')[1];
+ expect(th.className).toBeTruthy();
+ expect(td.className).toBe(th.className);
+ });
+
+ it('leaves the school-name column unclassed so it takes the spare width', () => {
+ const { container } = render();
+ expect(container.querySelectorAll('th')[0].className).toBe('');
+ });
+});
diff --git a/nextjs-app/components/places/PlaceView.module.css b/nextjs-app/components/places/PlaceView.module.css
index 3f69233..b304b37 100644
--- a/nextjs-app/components/places/PlaceView.module.css
+++ b/nextjs-app/components/places/PlaceView.module.css
@@ -144,10 +144,24 @@
border-bottom: none;
}
-.table th:last-child,
-.num {
+/*
+ * Header and value share one class and one rule, so they cannot drift apart.
+ *
+ * The first cut aligned them with two different selectors: `.table th:last-child`
+ * at (0,2,1) beat the element rule and went right, while `.num` at (0,1,0) lost
+ * to `.table td` at (0,1,1) and stayed left. The heading and its numbers sat on
+ * opposite edges of the column.
+ *
+ * width:1% with nowrap makes the measure column hug its content so the school
+ * name takes the remaining width — without it the two columns split evenly and
+ * the gap between heading and value reads as misalignment on a wide screen.
+ */
+.table th.num,
+.table td.num {
text-align: right;
font-variant-numeric: tabular-nums;
+ width: 1%;
+ white-space: nowrap;
}
/* The measure is spelled out; the tooltip carries the definition. */
diff --git a/nextjs-app/components/places/PlaceView.tsx b/nextjs-app/components/places/PlaceView.tsx
index 5c4b463..a8efc5d 100644
--- a/nextjs-app/components/places/PlaceView.tsx
+++ b/nextjs-app/components/places/PlaceView.tsx
@@ -71,7 +71,9 @@ function SchoolTable({ schools, phase }: { schools: School[]; phase: PhaseKey })
| School |
-
+ {/* Same class as the value cell below: one rule aligns both, so
+ they cannot drift apart. */}
+ |
{metric.heading}
|