From 01925857273b79655c0a120c50538c02cf5c9a5a Mon Sep 17 00:00:00 2001 From: serfa Date: Sun, 23 Aug 2026 14:01:34 -0400 Subject: [PATCH] Stock the trades we serve, and stop offering the ones we don't MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A demo hit "That's everyone nearby" on Electrician after four swipes. That was the deck working — there were exactly four — but the shape of the catalogue was worse than it looked: 42 of the 50 trades had NO pros at all, so tapping Roofer or Cleaner hit the dead end immediately rather than after seven swipes. Two halves to the fix, and the second matters more. Depth: 56 more pros, so the fifteen live trades now run 3–15 deep instead of 1–12. Every photo was picked by reading Unsplash's written description and keeping only images that show the trade being DONE, then checking each URL resolves — one of 33 was a 404 and was dropped rather than seeded broken. Where a photo already belonged to someone in this file the new pro takes a name of the same gender: the face and the name on a card have to agree. Honesty: the other 35 trades are seeded isActive:false. A category with nobody behind it is not a feature, it is the product claiming to do something it cannot — and no amount of invented supply fixes that, it just moves the lie one screen later. LIVE_TRADES is the list, and widening it means recruiting pros first and adding the slug second. Locksmith is called out in the file: photo searches return padlocks, not locksmiths, so two of its cards carry door hardware. A lock is at least a locksmith's work. A stock portrait of somebody who is plainly not one is not. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/components/chrome/project-panel.tsx | 138 +++++++++++++++++- packages/db/src/seed.ts | 111 +++++++++++++- packages/db/test/deck.test.ts | 3 + 3 files changed, 250 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chrome/project-panel.tsx b/apps/web/src/components/chrome/project-panel.tsx index 519f21c..3590265 100644 --- a/apps/web/src/components/chrome/project-panel.tsx +++ b/apps/web/src/components/chrome/project-panel.tsx @@ -69,6 +69,12 @@ const TRY = { } satisfies Record; const STACK: { group: Copy; items: string[] }[] = [ + // First, because it is the answer to the question the rest of this list + // provokes: fine, but what do I actually install on a phone? + { + group: { en: 'Mobile', es: 'Móvil' }, + items: ['Capacitor', 'iOS', 'Android'], + }, { group: { en: 'App', es: 'App' }, items: ['Next.js 15', 'React 19', 'TypeScript', 'Tailwind v4', 'Motion'], @@ -333,9 +339,82 @@ const HOSTING: { item: Copy; detail: Copy; usd: number }[] = [ const HOSTING_TOTAL = HOSTING.reduce((sum, h) => sum + h.usd, 0); +/** + * The third-party services the stack above names but never costed. + * + * Every one of these is free at launch volumes and none of them is free + * forever, so a flat monthly figure would be wrong in both directions. What + * matters to somebody deciding is the SHAPE of each bill — what has to happen + * before it starts, and what it climbs with. + */ +const SERVICES: { item: Copy; free: Copy; then: Copy }[] = [ + { + item: { en: 'Twilio', es: 'Twilio' }, + free: { en: 'Per message', es: 'Por mensaje' }, + then: { + en: 'No monthly fee, but every sign-in code and job alert is a few cents, plus a rented number. The only one that costs money from day one.', + es: 'Sin cuota mensual, pero cada código de acceso y cada aviso de trabajo cuesta unos céntimos, más un número alquilado. El único que cuesta dinero desde el primer día.', + }, + }, + { + item: { en: 'Mapbox', es: 'Mapbox' }, + free: { en: 'Free to start', es: 'Gratis al principio' }, + then: { + en: 'Free up to tens of thousands of address lookups a month. Climbs with searches, not with customers.', + es: 'Gratis hasta decenas de miles de búsquedas de direcciones al mes. Sube con las búsquedas, no con los clientes.', + }, + }, + { + item: { en: 'Resend', es: 'Resend' }, + free: { en: 'Free to start', es: 'Gratis al principio' }, + then: { + en: 'Free for the first few thousand emails a month, about $20 after that.', + es: 'Gratis para los primeros miles de correos al mes, unos 20 $ a partir de ahí.', + }, + }, + { + item: { en: 'Sentry', es: 'Sentry' }, + free: { en: 'Free to start', es: 'Gratis al principio' }, + then: { + en: 'Free tier covers early error volumes, around $26 once it does not. Optional — it reports crashes, it does not run anything.', + es: 'El plan gratuito cubre los primeros errores, unos 26 $ cuando deje de hacerlo. Opcional: informa de fallos, no ejecuta nada.', + }, + }, +]; + +/** + * What it costs to have the app exist in a store. + * + * Not our fee and not hosting — these are accounts in the client's own name, + * for the same reason the DigitalOcean account is: an app published under our + * developer account is an app they cannot take with them. + */ +const STORE_ACCOUNTS: { item: Copy; cost: Copy; detail: Copy }[] = [ + { + item: { en: 'Apple Developer Program', es: 'Apple Developer Program' }, + cost: { en: '$99 / year', es: '99 $ / año' }, + detail: { + en: 'Required to put anything on the App Store, and it lapses if unpaid — the app comes down with it. A company account also needs a D-U-N-S number, which is free but takes a couple of weeks, so it is worth starting early.', + es: 'Obligatoria para publicar cualquier cosa en la App Store, y caduca si no se renueva: la app se cae con ella. Una cuenta de empresa necesita además un número D-U-N-S, gratuito pero que tarda un par de semanas, así que conviene empezarlo pronto.', + }, + }, + { + item: { en: 'Google Play Console', es: 'Google Play Console' }, + cost: { en: '$25 once', es: '25 $ una vez' }, + detail: { + en: 'One payment, for the life of the account. Review is faster and less strict than Apple’s.', + es: 'Un solo pago, para toda la vida de la cuenta. La revisión es más rápida y menos estricta que la de Apple.', + }, + }, +]; + const SECTIONS = { features: { en: 'What it does', es: 'Qué hace' }, stack: { en: 'Built with', es: 'Hecho con' }, + stackBody: { + en: 'One codebase, three places to install it. The same app runs in a browser and ships to the App Store and Google Play wrapped in Capacitor — so there is one thing to build and one thing to fix, rather than a website and two native apps drifting apart.', + es: 'Un solo código, tres sitios donde instalarlo. La misma app funciona en el navegador y se publica en la App Store y en Google Play envuelta en Capacitor: hay una sola cosa que construir y una sola que arreglar, en lugar de una web y dos apps nativas que se van separando.', + }, decisions: { en: 'Still to decide', es: 'Aún por decidir' }, decisionsBody: { en: 'Everything below already has a behaviour. These are the ones worth choosing deliberately rather than inheriting.', @@ -356,6 +435,16 @@ const SECTIONS = { hosting: { en: 'Hosting, per month', es: 'Alojamiento, al mes' }, total: { en: 'Total', es: 'Total' }, perMonth: { en: '/mo', es: '/mes' }, + services: { en: 'Services it calls', es: 'Servicios que utiliza' }, + servicesNote: { + en: 'Separate accounts, separate bills, all in your name. None of these is charged by us and none has a markup.', + es: 'Cuentas separadas, facturas separadas, todas a tu nombre. Ninguno lo cobramos nosotros y ninguno lleva recargo.', + }, + stores: { en: 'App store accounts', es: 'Cuentas de las tiendas' }, + storesNote: { + en: 'The app is published under your developer accounts, not ours — the same reason the hosting is yours. An app on our account is an app you cannot take with you, and moving one afterwards means a new listing and losing its reviews and ranking.', + es: 'La app se publica con tus cuentas de desarrollador, no con las nuestras, por la misma razón que el alojamiento es tuyo. Una app en nuestra cuenta es una app que no te puedes llevar, y moverla después significa una ficha nueva y perder sus valoraciones y su posición.', + }, } satisfies Record; /** The three caveats under the hosting table — lead sentence, then the rest. */ @@ -367,6 +456,16 @@ const HOSTING_NOTES: { lead: Copy; rest: Copy }[] = [ es: ' No forma parte de nuestros honorarios y no lleva ningún recargo: la cuenta es tuya, así que puedes ver la factura y cambiar de plan sin pasar por nosotros.', }, }, + { + lead: { + en: 'DigitalOcean is the quote, not the requirement.', + es: 'DigitalOcean es el presupuesto, no el requisito.', + }, + rest: { + en: ' What runs is a container and a Postgres database, so it runs just as well on AWS, Google Cloud, Hetzner or whatever you already have an account with — only the figures above change. We price DigitalOcean because it is the cheapest of the managed options at this size and its bill is legible.', + es: ' Lo que se ejecuta es un contenedor y una base de datos Postgres, así que funciona igual de bien en AWS, Google Cloud, Hetzner o donde ya tengas cuenta: solo cambian las cifras de arriba. Presupuestamos DigitalOcean porque es la más barata de las opciones gestionadas a este tamaño y su factura se entiende.', + }, + }, { lead: { en: `$${HOSTING_TOTAL} is the smallest tier of each.`, @@ -477,7 +576,8 @@ export function ProjectPanel() {
-

{t(SECTIONS.stack)}

+

{t(SECTIONS.stack)}

+

{t(SECTIONS.stackBody)}

{STACK.map((row) => (
@@ -567,6 +667,42 @@ export function ProjectPanel() {

))}
+ + {/* + The stack above names five outside services and the table above costs + none of them. No running total here on purpose: four services that are + each free until a different threshold do not add up to a number, and + printing one would be a figure nobody could check. + */} +

{t(SECTIONS.services)}

+
    + {SERVICES.map((s) => ( +
  • +
    + {t(s.item)} + + {t(s.free)} +
    +

    {t(s.then)}

    +
  • + ))} +
+

{t(SECTIONS.servicesNote)}

+ +

{t(SECTIONS.stores)}

+
    + {STORE_ACCOUNTS.map((s) => ( +
  • +
    + {t(s.item)} + + {t(s.cost)} +
    +

    {t(s.detail)}

    +
  • + ))} +
+

{t(SECTIONS.storesNote)}

); diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index b9adaaa..26d7652 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -74,6 +74,23 @@ function offset(lat: number, lng: number, metres: number, bearingDeg: number) { * stable key (pros, jobs and tests reference them); names and icons are cosmetic. * Icons are lucide names, kebab-case. */ +/** + * The trades this marketplace can actually serve. + * + * A category with no pros behind it is not a feature, it is a dead end: the + * customer picks it, the deck is empty, and the product has told them it does + * something it does not do. Everything outside this set is seeded `isActive: + * false` and never reaches the trade strip or search. + * + * This is the honest shape of a launch market. Widening it means recruiting + * supply first, then adding the slug here — in that order. + */ +const LIVE_TRADES = new Set([ + 'plumber', 'electrician', 'handyman', 'painter', 'carpenter', 'locksmith', + 'appliance-repair', 'hvac', 'cleaner', 'gardener', 'mover', 'builder', + 'roofer', 'window-cleaner', 'pest-control', +]); + const CATEGORIES = [ // Core trades — the eight that carry most of the volume. { slug: 'plumber', name: 'Plumber', icon: 'shower-head' }, @@ -211,6 +228,98 @@ const PROS: SeedPro[] = [ { name: 'Unverified Ulises', cat: 'plumber', photo: 'photo-1749532125405-70950966b0e5', distanceM: 1_000, rating: null, reviews: 0, radius: 15_000, unverified: true }, // Verified but on holiday — must never reach a deck. { name: 'Away Arturo', cat: 'plumber', photo: 'photo-1676210134188-4c05dd172f89', distanceM: 1_100, rating: 4.9, reviews: 20, radius: 15_000, away: true }, + + /* + * Depth, added after a demo ran a trade dry in four swipes. + * + * Every photo below was chosen by reading Unsplash's written description and + * keeping only images that show the trade being done, then checking that each + * URL resolves. Where a trade reuses a photo already in this file, the new pro + * takes a name of the same gender as the existing one — the face and the name + * on a card have to agree. + */ + + // ---- more of the trades that already had supply ---- + { name: 'Ramiro Escalante', cat: 'plumber', photo: 'photo-1676210133055-eab6ef033ce3', distanceM: 3_700, rating: 4.6, reviews: 26, radius: 18_000 }, + { name: 'Efraín Nava', cat: 'plumber', photo: 'photo-1749532125405-70950966b0e5', distanceM: 6_200, rating: 4.8, reviews: 51, radius: 22_000 }, + { name: 'Ismael Zúñiga', cat: 'plumber', photo: 'photo-1676210134190-3f2c0d5cf58d', distanceM: 1_300, rating: 4.7, reviews: 34, radius: 15_000 }, + + { name: 'Rubén Alcántara', cat: 'electrician', photo: 'photo-1660330589693-99889d60181e', distanceM: 5_400, rating: 4.7, reviews: 39, radius: 22_000 }, + { name: 'Teresa Alcalá', cat: 'electrician', photo: 'photo-1646640381839-02748ae8ddf0', distanceM: 3_100, rating: 4.8, reviews: 45, radius: 20_000 }, + { name: 'Hugo Peralta', cat: 'electrician', photo: 'photo-1621905251189-08b45d6a269e', distanceM: 7_600, rating: 4.5, reviews: 18, radius: 25_000 }, + + { name: 'Fidel Barrera', cat: 'handyman', photo: 'photo-1621905251918-48416bd8575a', distanceM: 2_700, rating: 4.6, reviews: 30, radius: 15_000 }, + { name: 'Ramón Ocampo', cat: 'handyman', photo: 'photo-1698998882494-57c3e043f340', distanceM: 5_800, rating: 4.7, reviews: 43, radius: 20_000 }, + { name: 'Ernesto Lira', cat: 'handyman', photo: 'photo-1621905251918-48416bd8575a', distanceM: 8_900, rating: 4.4, reviews: 13, radius: 25_000 }, + + { name: 'Verónica Pacheco', cat: 'painter', photo: 'photo-1717281234297-3def5ae3eee1', distanceM: 4_200, rating: 4.8, reviews: 48, radius: 20_000 }, + { name: 'Salvador Ibarra', cat: 'painter', photo: 'photo-1652829069834-2c05031199c5', distanceM: 6_700, rating: 4.6, reviews: 25, radius: 22_000 }, + { name: 'Lorena Quintero', cat: 'painter', photo: 'photo-1717281234297-3def5ae3eee1', distanceM: 1_700, rating: 4.9, reviews: 59, radius: 15_000 }, + + { name: 'Emilio Cervantes', cat: 'carpenter', photo: 'photo-1544164560-adac3045edb2', distanceM: 3_800, rating: 4.7, reviews: 36, radius: 20_000 }, + { name: 'Patricia Aguirre', cat: 'carpenter', photo: 'photo-1659930087003-2d64e33181f7', distanceM: 6_300, rating: 4.8, reviews: 54, radius: 25_000 }, + { name: 'Raúl Mejía', cat: 'carpenter', photo: 'photo-1544164560-adac3045edb2', distanceM: 2_200, rating: 4.5, reviews: 20, radius: 18_000 }, + + // Locksmith is the thinnest trade for photography — the searches return + // padlocks, not locksmiths. Two door-hardware shots carry the extra pros + // rather than a stock portrait of somebody plainly not a locksmith. + { name: 'Aarón Valdés', cat: 'locksmith', photo: 'photo-1579382311054-0701b08da226', distanceM: 2_900, rating: 4.7, reviews: 33, radius: 22_000 }, + { name: 'Óscar Lozano', cat: 'locksmith', photo: 'photo-1616358284394-acded77dcea5', distanceM: 6_100, rating: 4.6, reviews: 24, radius: 25_000 }, + { name: 'Felipe Carrillo', cat: 'locksmith', photo: 'photo-1676630656246-3047520adfdf', distanceM: 1_400, rating: 4.9, reviews: 58, radius: 18_000 }, + + { name: 'Alfonso Arriaga', cat: 'appliance-repair', photo: 'photo-1621905251918-48416bd8575a', distanceM: 4_600, rating: 4.7, reviews: 32, radius: 20_000 }, + { name: 'Mauricio Tapia', cat: 'appliance-repair', photo: 'photo-1698998882494-57c3e043f340', distanceM: 7_200, rating: 4.6, reviews: 22, radius: 25_000 }, + { name: 'Octavio Lugo', cat: 'appliance-repair', photo: 'photo-1621905251918-48416bd8575a', distanceM: 2_500, rating: 4.8, reviews: 46, radius: 18_000 }, + + { name: 'Damián Solórzano', cat: 'hvac', photo: 'photo-1642749776312-aa42ce20c9f5', distanceM: 3_300, rating: 4.7, reviews: 37, radius: 22_000 }, + { name: 'Fermín Villaseñor', cat: 'hvac', photo: 'photo-1705579605238-24a90c8799c5', distanceM: 6_900, rating: 4.8, reviews: 50, radius: 25_000 }, + { name: 'Joaquín Bañuelos', cat: 'hvac', photo: 'photo-1642749776312-aa42ce20c9f5', distanceM: 1_100, rating: 4.5, reviews: 21, radius: 15_000 }, + + // ---- trades that had no supply at all until now ---- + { name: 'Guadalupe Rentería', cat: 'cleaner', photo: 'photo-1646980241033-cd7abda2ee88', distanceM: 1_400, rating: 4.8, reviews: 63, radius: 15_000, + skills: ['Deep cleans', 'Move-out cleans'] }, + { name: 'Alma Trejo', cat: 'cleaner', photo: 'photo-1647381518264-97ff1835026f', distanceM: 3_200, rating: 4.7, reviews: 41, radius: 18_000 }, + { name: 'Rocío Delgado', cat: 'cleaner', photo: 'photo-1758273238415-01ec03d9ef27', distanceM: 5_600, rating: 4.9, reviews: 88, radius: 20_000 }, + { name: 'Isabel Mora', cat: 'cleaner', photo: 'photo-1758523670739-0d26a3ee976d', distanceM: 2_100, rating: 4.6, reviews: 24, radius: 12_000 }, + { name: 'Yolanda Cárdenas', cat: 'cleaner', photo: 'photo-1758272421751-963195322eaa', distanceM: 7_300, rating: 4.5, reviews: 17, radius: 25_000 }, + + { name: 'Tomás Vega', cat: 'gardener', photo: 'photo-1637531347055-4fa8aa80c111', distanceM: 2_300, rating: 4.8, reviews: 47, radius: 20_000, + skills: ['Tree surgery', 'Irrigation'] }, + { name: 'Beatriz Olvera', cat: 'gardener', photo: 'photo-1555955208-94f6fafea771', distanceM: 4_700, rating: 4.7, reviews: 29, radius: 18_000 }, + { name: 'Aureliano Sosa', cat: 'gardener', photo: 'photo-1605117882932-f9e32b03fea9', distanceM: 6_100, rating: 4.5, reviews: 15, radius: 25_000 }, + { name: 'Margarita Nieto', cat: 'gardener', photo: 'photo-1728706613021-e447801e1ea6', distanceM: 1_900, rating: 4.9, reviews: 66, radius: 15_000 }, + { name: 'Everardo Rangel', cat: 'gardener', photo: 'photo-1621460249485-4e4f92c9de5d', distanceM: 8_200, rating: 4.3, reviews: 9, radius: 30_000 }, + + { name: 'Baltazar Nájera', cat: 'mover', photo: 'photo-1698917414969-feade59e3343', distanceM: 3_400, rating: 4.6, reviews: 38, radius: 40_000 }, + { name: 'Rigoberto Cuevas', cat: 'mover', photo: 'photo-1694715669993-ea0022b470f7', distanceM: 5_900, rating: 4.8, reviews: 55, radius: 45_000, + skills: ['Piano moving', 'Packing service'] }, + { name: 'Nicolás Frías', cat: 'mover', photo: 'photo-1523543659209-5c57c05834aa', distanceM: 2_600, rating: 4.5, reviews: 19, radius: 35_000 }, + { name: 'Hilario Ordaz', cat: 'mover', photo: 'photo-1642756457381-930fdc1e2e2e', distanceM: 7_700, rating: 4.7, reviews: 44, radius: 50_000 }, + { name: 'Damián Bravo', cat: 'mover', photo: 'photo-1554620158-d8d5c2f3a27b', distanceM: 1_800, rating: 4.4, reviews: 11, radius: 30_000 }, + + { name: 'Heriberto Alanís', cat: 'builder', photo: 'photo-1587582423116-ec07293f0395', distanceM: 4_300, rating: 4.6, reviews: 27, radius: 35_000 }, + { name: 'Anselmo Padilla', cat: 'builder', photo: 'photo-1563166423-482a8c14b2d6', distanceM: 7_100, rating: 4.8, reviews: 61, radius: 40_000, + skills: ['Extensions', 'Structural work'] }, + { name: 'Ezequiel Maldonado', cat: 'builder', photo: 'photo-1593313637552-29c2c0dacd35', distanceM: 2_400, rating: 4.7, reviews: 35, radius: 25_000 }, + { name: 'Leonardo Gallardo', cat: 'builder', photo: 'photo-1667207591431-0366faabde1a', distanceM: 10_300, rating: 4.5, reviews: 20, radius: 45_000 }, + { name: 'Porfirio Escobar', cat: 'builder', photo: 'photo-1530639834082-05bafb67fbbe', distanceM: 1_500, rating: 4.9, reviews: 73, radius: 30_000 }, + + { name: 'Abel Montoya', cat: 'roofer', photo: 'photo-1635424824849-1b09bdcc55b1', distanceM: 4_100, rating: 4.7, reviews: 33, radius: 25_000 }, + { name: 'Cristóbal Ruelas', cat: 'roofer', photo: 'photo-1726589004565-bedfba94d3a2', distanceM: 6_800, rating: 4.6, reviews: 21, radius: 30_000 }, + { name: 'Genaro Ibáñez', cat: 'roofer', photo: 'photo-1633759593085-1eaeb724fc88', distanceM: 2_900, rating: 4.9, reviews: 52, radius: 20_000, + skills: ['Flat roofs', 'Leak tracing'] }, + { name: 'Moisés Contreras', cat: 'roofer', photo: 'photo-1635424709961-f3a150459ad4', distanceM: 9_400, rating: 4.4, reviews: 12, radius: 35_000 }, + { name: 'Ulises Barrios', cat: 'roofer', photo: 'photo-1635424824800-692767998d07', distanceM: 1_600, rating: 4.8, reviews: 40, radius: 18_000 }, + + { name: 'Efrén Aparicio', cat: 'window-cleaner', photo: 'photo-1635445818409-64a0ff92eb39', distanceM: 2_800, rating: 4.7, reviews: 31, radius: 20_000 }, + { name: 'Salomón Terán', cat: 'window-cleaner', photo: 'photo-1775654063422-54e0a71578ad', distanceM: 5_100, rating: 4.8, reviews: 49, radius: 25_000 }, + { name: 'Bernardo Quiroz', cat: 'window-cleaner', photo: 'photo-1719499757876-346424e8f67d', distanceM: 8_600, rating: 4.5, reviews: 16, radius: 30_000 }, + + { name: 'Rodolfo Zavala', cat: 'pest-control', photo: 'photo-1747659629851-a92bd71149f6', distanceM: 3_600, rating: 4.7, reviews: 42, radius: 25_000 }, + { name: 'Ignacio Bermúdez', cat: 'pest-control', photo: 'photo-1674485135526-b5a686b33dfe', distanceM: 6_400, rating: 4.6, reviews: 23, radius: 30_000 }, + { name: 'Faustino Ayala', cat: 'pest-control', photo: 'photo-1749030415358-f533ad412767', distanceM: 2_000, rating: 4.8, reviews: 57, radius: 20_000 }, + { name: 'Gustavo Meraz', cat: 'pest-control', photo: 'photo-1670989292166-8b20b9530438', distanceM: 9_100, rating: 4.4, reviews: 14, radius: 35_000 }, + ]; /** @@ -377,7 +486,7 @@ async function main() { const cats = await db .insert(schema.categories) - .values(CATEGORIES.map((c, i) => ({ ...c, position: i }))) + .values(CATEGORIES.map((c, i) => ({ ...c, position: i, isActive: LIVE_TRADES.has(c.slug) }))) .returning(); const catBySlug = new Map(cats.map((c) => [c.slug, c.id])); console.log(` ${cats.length} categories`); diff --git a/packages/db/test/deck.test.ts b/packages/db/test/deck.test.ts index 1d8dd68..6bcb7cd 100644 --- a/packages/db/test/deck.test.ts +++ b/packages/db/test/deck.test.ts @@ -55,12 +55,15 @@ describe('getDeck', () => { expect(names).toEqual([ 'Alejandro Dávila', 'Antón Bautista', + 'Efraín Nava', 'Gabriel Torres', 'Gerardo Solís', + 'Ismael Zúñiga', 'Jorge Pineda', // Sorted by code unit, so accented letters land after plain ASCII ones. 'Norma Salgado', 'Néstor Bosque', + 'Ramiro Escalante', 'Rogelio Amaya', 'Sergio Fabela', ]);