diff --git a/web/app/(site)/join/page.tsx b/web/app/(site)/join/page.tsx
index 327ab8d..1379b3c 100644
--- a/web/app/(site)/join/page.tsx
+++ b/web/app/(site)/join/page.tsx
@@ -10,6 +10,11 @@ import Note from "@/components/fx/Note";
// the next action, and a closing "here is what to do next" band underneath a
// half-filled form is an invitation to abandon it.
//
+// It does now close on a band, but the distinction above is what that band is
+// built to respect — it contains no link, so it argues for the form rather than
+// offering somewhere else to be. Anything added down there has to clear the same
+// bar: no exits under an unfinished form.
+//
// It is also absent from PAGES, so it appears in neither the nav strip nor beside
// the other routes in the footer. The nav carries it as a filled button at the
// other end of the bar — putting the same word in the strip as well would be the
@@ -180,6 +185,60 @@ export default function Join() {
+
+ {/* ---- The two futures ---------------------------------------------
+ The closing band, and the exception the comment at the top of this
+ file now carries: it is not a NextAction. Every other route ends by
+ handing you somewhere else to go, which under a half-filled form
+ would be an exit. This band has no link in it at all. It restates
+ the decision already on the screen and then stops, so the only
+ thing to do with it is scroll back up and finish.
+
+ NOT INSIDE ApplyForm. It lived in the card for one revision and the
+ card is the wrong container: at 15px inside a 7-unit padded tile it
+ read as a third disclaimer under the two grey notes about data, and
+ disclaimers are what people skip. On the page at display-md it is
+ the loudest type below the hero.
+
+ The asymmetry is the whole argument. Both eyebrows take .label, so
+ they read as one pair rather than a warning and a reward — the
+ weight sits in the body copy instead: `dust` on the left, `ink` on
+ the right, plus the accent rule down the right column. The future
+ where you did nothing is literally the dimmer of the two.
+ Do not try to colour the eyebrow to sharpen it: .label sets its own
+ colour and every custom class in globals.css is declared after
+ @tailwind utilities, so a text-* utility on it silently does
+ nothing. See the note on .page-top there. */}
+
+ {/* One column until lg, and lg rather than sm because these are
+ 30-word paragraphs at display size — the point where two of them
+ fit side by side without either dropping to four words a line is
+ a good deal wider than the point where two columns fit. */}
+
+
+
If you close this tab without applying
+
+ You'll open it again in February. The same form, one semester
+ less, and a batch of students who already know how to review your
+ code.
+
+
+
+ {/* The accent rule is the second column's left edge at lg and its top
+ edge below that, so the band never loses the mark that says which
+ of the two futures the page is pointing at. */}
+
+
If you hit the button
+
+ Someone reads it this week. You show up regularly. By November
+ you're the one answering the questions.
+
+
+
+
);
}
diff --git a/web/app/(site)/programmes/page.tsx b/web/app/(site)/programmes/page.tsx
index 67f14ec..a97f64e 100644
--- a/web/app/(site)/programmes/page.tsx
+++ b/web/app/(site)/programmes/page.tsx
@@ -136,8 +136,18 @@ function ProgrammeField({ p }: { p: ProgrammeInfo }) {
Who from SST has done it
+ {/* The written answer, for programmes where a count and a standout
+ name say more than a list of selections could — see `ours` in
+ ProgrammeInfo. It sits above the derived names rather than
+ replacing them, so adding a selection later cannot silently drop
+ this line, and this line cannot silently hide a selection. */}
+ {p.ours && {p.ours}
}
{ours.length > 0 ? (
-
{member.github && (
@@ -709,12 +723,21 @@ export default function Team() {
gridTemplateColumns: `repeat(${DESK.members.length}, minmax(0, 1fr))`,
}}
>
+ {/* NO REMIT PASSED DOWN, per the club. The desk's remit used to ride
+ on every member's hover card, which meant four faces in a row each
+ popping the same paragraph — the sentence is about the desk, not
+ about the person under the pointer, and reading it four times is
+ how you learn to stop hovering. It has not been deleted: the desk
+ label above these four still stands for it in the chart, and the
+ stacked list below writes it out once, in full, where it is read
+ aloud and where touch devices get it. What is left on hover is only
+ what belongs to the person — their batch and their highlights — and
+ a member with neither now gets no bubble at all. */}
{DESK.members.map((m, i) => (
))}
diff --git a/web/components/hall/ContribWall.tsx b/web/components/hall/ContribWall.tsx
index 42612ca..b6b7820 100644
--- a/web/components/hall/ContribWall.tsx
+++ b/web/components/hall/ContribWall.tsx
@@ -22,7 +22,7 @@
//
// * It is NOT per-student. One wall under the whole grid reads as a section
// motif; a strip behind each card would read as that person's activity, and
-// none of these fifteen has activity data recorded anywhere in this repo.
+// none of these students has activity data recorded anywhere in this repo.
//
// * The pattern is DETERMINISTIC, from a tiny integer hash rather than
// Math.random(). Random would differ between the server render and the client
diff --git a/web/components/hall/Hall.tsx b/web/components/hall/Hall.tsx
index 6010890..9a60f10 100644
--- a/web/components/hall/Hall.tsx
+++ b/web/components/hall/Hall.tsx
@@ -345,7 +345,8 @@ export default function Hall() {
{/* THE OPEN SLOT, last in the grid.
Every other card is a record of something that already happened, which
- makes the whole grid readable as a closed set — fifteen people, done. This
+ makes the whole grid readable as a closed set — this is who got picked,
+ done. This
one card says the list is still being written, and it is the only card here
that is an invitation rather than evidence.
diff --git a/web/components/hero/Terminal.tsx b/web/components/hero/Terminal.tsx
index a35f89f..abe7587 100644
--- a/web/components/hero/Terminal.tsx
+++ b/web/components/hero/Terminal.tsx
@@ -24,7 +24,7 @@
// The evidence did not go anywhere. It moved to where it was already stronger:
// the floating chips still carry the API-verified merge count and the cohort
// size (see Hero.tsx), the figure block under the hero states the total, and the
-// hall names all fifteen with links. Nobody lost a credit; the credit stopped
+// hall names every one of them with links. Nobody lost a credit; the credit stopped
// being a leaderboard.
//
// NOTHING HERE IS A CLAIM. That is the other thing the change buys. A simulated
diff --git a/web/content/club.ts b/web/content/club.ts
index 9f7f38e..b6a27d1 100644
--- a/web/content/club.ts
+++ b/web/content/club.ts
@@ -385,6 +385,25 @@ const GSOC_2025: [name: string, studyYear: string, org?: string][] = [
["Sauhard Gupta", "3rd year"],
];
+/* The first selections outside GSoC, and the reason the wall stops being a GSoC
+ wall. Same shape and same rules as the arrays above: the org slot stays empty
+ until the club supplies it, and the card prints "org TBA" rather than a guess.
+
+ VIVEK SINGH SOLANKI APPEARS TWICE ON PURPOSE — once for GSoC 2026 above and once
+ for LFX below. Two selections into two programmes are two facts, and the wall is
+ a list of selections, not of people. Everything that counts PEOPLE dedupes by
+ name already (see NumbersStrip), so this adds a card without inflating the
+ member count. Note also that he is a different person from the "Vivek Singh"
+ selected into Summer of Bitcoin; the names are close enough that a future editor
+ will wonder, so: not a duplicate, and not a typo. */
+const SOB_2026: [name: string, studyYear: string, org?: string][] = [
+ ["Vivek Singh", "2nd year"],
+];
+
+const LFX_2026: [name: string, studyYear: string, org?: string][] = [
+ ["Vivek Singh Solanki", "3rd year", "Besu"],
+];
+
/**
* The published list. These render everywhere — local, preview and production.
*
@@ -399,16 +418,17 @@ const GSOC_2025: [name: string, studyYear: string, org?: string][] = [
* WHAT IS STILL MISSING, and it belongs here rather than in a ticket: not one of
* these entries has a proof `url`. The page's entire argument is "somebody else
* picked them, go and check", and until each name carries a link — the programme's
- * accepted-projects page, the student's proposal, the announcement — these fifteen
- * are the only claims on this site a reader cannot verify. The roster prints an em
- * dash in the Proof column for each, which is honest but is not evidence. Add `url`
- * per entry as the links come in; nothing else has to change.
+ * accepted-projects page, the student's proposal, the announcement — these are the
+ * only claims on this site a reader cannot verify. The roster prints an em dash in
+ * the Proof column for each, which is honest but is not evidence. Add `url` per
+ * entry as the links come in; nothing else has to change.
*
* The ten LFX / C4GT / SoB rows that used to pad this list to twenty-five are gone,
* deliberately. "Placeholder Seven, Example Foundation" is layout padding, not a
* selected student; publishing it would have put visible nonsense on a public page
- * beside fifteen real people and undercut every real name next to it. Those
- * programmes get real entries the same way these did.
+ * beside real people and undercut every real name next to it. Those programmes get
+ * real entries the same way these did — the SoB and LFX rows below are the first of
+ * them, supplied by the club per student rather than generated to fill the grid.
*/
export const SELECTIONS: Selection[] = [
...GSOC_2026.map(([name, studyYear, org]) => ({
@@ -427,6 +447,22 @@ export const SELECTIONS: Selection[] = [
org,
consented: true,
})),
+ ...SOB_2026.map(([name, studyYear, org]) => ({
+ name,
+ programme: "SOB" as Programme,
+ year: "2026",
+ studyYear,
+ org,
+ consented: true,
+ })),
+ ...LFX_2026.map(([name, studyYear, org]) => ({
+ name,
+ programme: "LFX" as Programme,
+ year: "2026",
+ studyYear,
+ org,
+ consented: true,
+ })),
];
/**
@@ -525,6 +561,20 @@ export type ProgrammeInfo = {
pays: string;
/** What OSC specifically does to get you in. This is the product. */
weDo: string;
+ /**
+ * What the club has to show for this programme, when a list of names is the
+ * wrong shape for it.
+ *
+ * The "who from SST has done it" row is normally DERIVED from SELECTIONS, and
+ * that stays the default: a selective programme's answer is a set of people who
+ * were each individually picked, and deriving it is what stops this page and the
+ * Hall of Fame disagreeing. But the open tier is a participation programme —
+ * dozens take part and there is nothing to be selected into — so the honest
+ * answer is a count plus whoever stood out, which no list of Selection rows can
+ * express without inventing a selection that never happened. Prose here, names
+ * in SELECTIONS; when both exist the row shows both.
+ */
+ ours?: string;
url: string;
};
@@ -589,6 +639,8 @@ export const PROGRAMMES: ProgrammeInfo[] = [
"No stipend. Certificates, swag and a leaderboard — plus mentors, which is the part that is actually worth having.",
weDo:
"Nothing to prepare. Register when it opens and pick a project in a language you can already run. Use it to learn the mechanics — fork, branch, PR, review, merge — so the paid programmes below are not your first time using Git in anger.",
+ ours:
+ "30+ students participated in GSSoC '26. Top contributor from SST: Bhumi N Deshpande.",
url: "https://gssoc.girlscript.tech/",
},
{
@@ -601,6 +653,7 @@ export const PROGRAMMES: ProgrammeInfo[] = [
pays: "Swag, or a tree planted in your name. That is the whole reward.",
weDo:
"Have one project picked and built on your machine before 1 October, so you spend the month contributing rather than setting up. Come to a session in September and we will do it with you.",
+ ours: "Many of us had our first PRs merged here.",
url: "https://hacktoberfest.com/",
},
];