/* ============================================================
   PINKPONY UNIVERSITY — training module (guides, videos,
   checklists, glossary, FAQ). Progress in localStorage: pp_univ.
   Real staff names replaced with roles (never version labor data).
   ============================================================ */

function univData(t) {
  const MODULES = [
    {
      id: "mod-1-1", cat: t("Daily Operations", "Operaciones Diarias"), icon: "vault",
      title: t("Opening the Safe", "Apertura de Caja"), level: t("Basic", "Básico"), mins: 5,
      desc: t("Count and register Big Safe and Small Safe at the start of each shift.", "Cuenta y registra el Big Safe y Small Safe al inicio de cada turno."),
      steps: [
        { title: t("Locate both safes", "Localiza las dos cajas"), body: t("Before anything else, physically locate the Big Safe (large vault) and the Small Safe (petty cash box). Both must be counted every single day before operations begin.", "Antes de cualquier cosa, localiza físicamente el Big Safe (caja grande) y el Small Safe (caja chica). Ambas deben contarse todos los días antes de comenzar operaciones.") },
        { title: t("Count Big Safe", "Cuenta el Big Safe"), body: t("Count all bills in the Big Safe. Group by denomination. Write the total on paper first, then enter it in the system under Cash Control → Big Safe Opening.", "Cuenta todos los billetes del Big Safe. Agrupa por denominación. Escribe el total en papel primero, luego ingrésalo en el sistema en Cash Control → Apertura Big Safe."), rule: t("The Big Safe opening amount must match exactly the closing amount from the previous night.", "El monto de apertura del Big Safe debe coincidir exactamente con el cierre de la noche anterior.") },
        { title: t("Count Small Safe", "Cuenta el Small Safe"), body: t("Repeat the same process for the Small Safe. This is the operational petty cash used during the night for small expenses.", "Repite el mismo proceso para el Small Safe. Este es el efectivo operativo usado durante la noche para gastos pequeños.") },
        { title: t("Enter amounts in the system", "Ingresa los montos en el sistema"), body: t("Go to Admin Portal → Cash Control. Enter Big Safe amount in Line 2 and Small Safe amount in Line 3. The system will automatically calculate the total opening balance.", "Ve al Portal Admin → Cash Control. Ingresa el monto del Big Safe en Línea 2 y el Small Safe en Línea 3. El sistema calculará automáticamente el saldo total de apertura.") },
        { title: t("Verify carry-forward", "Verifica el carry-forward"), body: t("The system shows the expected opening (carried from last night's close). If your count matches — green checkmark. If not — the system will ask you to register a Short or Credit with a note.", "El sistema muestra la apertura esperada (arrastrada del cierre de anoche). Si tu conteo coincide — palomita verde. Si no — el sistema te pedirá registrar un Short o Credit con nota.") },
      ],
    },
    {
      id: "mod-1-2", cat: t("Daily Operations", "Operaciones Diarias"), icon: "sparkles",
      title: t("Dancer Check-In", "Registro de Bailarinas"), level: t("Basic", "Básico"), mins: 8,
      desc: t("How to check in dancers, register dances, and calculate House Fee correctly.", "Cómo hacer check-in de bailarinas, registrar bailes y calcular el House Fee correctamente."),
      steps: [
        { title: t("Why this matters", "Por qué esto importa"), body: t("Every dancer's House Fee feeds directly into the nightly cash total. If a dancer is not registered in the system, her House Fee is NOT counted and the safe will show a short at closing.", "El House Fee de cada bailarina alimenta directamente el total de efectivo nocturno. Si una bailarina no está registrada en el sistema, su House Fee NO se cuenta y la caja mostrará un faltante al cierre."), rule: t("ALL dancers must check in through the system. No exceptions.", "TODAS las bailarinas deben hacer check-in por el sistema. Sin excepciones.") },
        { title: t("Dancer check-in process", "Proceso de check-in"), body: t("Go to Admin Portal → Dancers. Click 'Check In'. Select the dancer's name from the list. The system records her entry time automatically.", "Ve al Portal Admin → Dancers. Click 'Check In'. Selecciona el nombre de la bailarina de la lista. El sistema registra su hora de entrada automáticamente.") },
        { title: t("Registering private dances", "Registrar bailes privados"), body: t("Each private dance must be logged in the system immediately after it occurs. Go to Dancers → select dancer → Add Dance. Enter the room number and amount.", "Cada baile privado debe registrarse en el sistema inmediatamente después de que ocurra. Ve a Dancers → selecciona bailarina → Agregar Baile. Ingresa el número de cuarto y monto.") },
        { title: t("House Fee calculation", "Cálculo del House Fee"), body: t("The system automatically calculates: House Fee = Entry Fee + Sum of all registered dances. This total is added to the nightly cash expected in the safe.", "El sistema calcula automáticamente: House Fee = Tarifa de Entrada + Suma de todos los bailes registrados. Este total se suma al efectivo nocturno esperado en caja.") },
        { title: t("End of night dancer report", "Reporte de bailarinas al cierre"), body: t("At closing, go to Admin Portal → Dancers → Daily Report. This shows each dancer's entry time, number of dances, and total House Fee paid. Verify this matches the cash collected.", "Al cierre, ve al Portal Admin → Dancers → Reporte Diario. Muestra la hora de entrada de cada bailarina, número de bailes y House Fee total pagado. Verifica que coincida con el efectivo recolectado.") },
      ],
    },
    {
      id: "mod-1-3", cat: t("Daily Operations", "Operaciones Diarias"), icon: "door-open",
      title: t("Door Portal", "Portal de Puerta"), level: t("Basic", "Básico"), mins: 5,
      desc: t("Register covers, parking, and entries — separating cash from card payments.", "Registra covers, parking y entradas — separando pagos en efectivo de tarjeta."),
      steps: [
        { title: t("Access the Door Portal", "Accede al Portal de Puerta"), body: t("The door staff logs into their dedicated portal. This is separate from the Admin Portal. Same app, role: Door Staff.", "El staff de puerta inicia sesión en su portal dedicado. Es separado del Portal Admin. Misma app, rol: Door Staff.") },
        { title: t("Register each entry", "Registra cada entrada"), body: t("For each customer entering: tap 'New Entry', select payment type (Cash or Card), enter the cover amount. The system timestamps each entry automatically.", "Por cada cliente que entra: toca 'Nueva Entrada', selecciona tipo de pago (Efectivo o Tarjeta), ingresa el monto del cover. El sistema registra la hora automáticamente.") },
        { title: t("Register parking", "Registra parking"), body: t("For parking fees: tap 'Parking', select Cash or Card, enter amount. Parking cash goes to the safe. Parking by card goes to Toast totals.", "Para cobros de parking: toca 'Parking', selecciona Efectivo o Tarjeta, ingresa monto. El efectivo de parking va a la caja. El parking por tarjeta va a los totales de Toast.") },
        { title: t("Cash vs Card — the key rule", "Efectivo vs Tarjeta — la regla clave"), body: t("Cash collected at the door feeds the nightly safe total. Card payments go through Toast and appear in Net Sales. Never mix them.", "El efectivo recolectado en la puerta alimenta el total de caja nocturno. Los pagos con tarjeta van por Toast y aparecen en Net Sales. Nunca los mezcles."), rule: t("Private dances (dancers) are ALWAYS cash only. No card payments for dances.", "Los bailes privados (bailarinas) son SIEMPRE solo efectivo. Sin pagos con tarjeta para bailes.") },
      ],
    },
    {
      id: "mod-1-4", cat: t("Daily Operations", "Operaciones Diarias"), icon: "clipboard-check",
      title: t("Employee Close Report", "Cierre de Turno por Empleado"), level: t("Intermediate", "Intermedio"), mins: 10,
      desc: t("Generate individual close reports for each server and bartender at end of shift.", "Genera reportes de cierre individuales para cada server y bartender al final del turno."),
      steps: [
        { title: t("What is a Close Report", "Qué es un Close Report"), body: t("A Close Report is a per-employee summary generated at end of shift. It shows: total sales, cash drop expected, tips, service charge, and house percentage.", "Un Close Report es un resumen por empleado generado al final del turno. Muestra: ventas totales, cash drop esperado, propinas, service charge y porcentaje de casa.") },
        { title: t("Generate the report", "Genera el reporte"), body: t("Go to Admin Portal → Staff → select employee → End Shift → Generate Close Report. The system pulls all sales data from Toast for that employee automatically.", "Ve al Portal Admin → Staff → selecciona empleado → Terminar Turno → Generar Close Report. El sistema jala automáticamente todos los datos de ventas de Toast para ese empleado.") },
        { title: t("Review with the employee", "Revisa con el empleado"), body: t("Show the employee their Close Report. The Cash Drop line shows exactly how much cash they must hand over. This number is calculated — they cannot dispute it.", "Muéstrale al empleado su Close Report. La línea de Cash Drop muestra exactamente cuánto efectivo deben entregar. Este número es calculado — no pueden disputarlo.") },
        { title: t("Confirm the cash drop", "Confirma el cash drop"), body: t("Count the cash the employee hands over. If it matches the Close Report → tap 'Confirm'. If it doesn't match → tap 'Short' or 'Credit' and enter the actual amount with a mandatory note.", "Cuenta el efectivo que entrega el empleado. Si coincide con el Close Report → toca 'Confirmar'. Si no coincide → toca 'Short' o 'Credit' e ingresa el monto real con nota obligatoria."), rule: t("Server house %: 17%. Bartender house %: 13%. These percentages are locked — no one can change them manually.", "Porcentaje de casa para servers: 17%. Para bartenders: 13%. Estos porcentajes están bloqueados — nadie puede cambiarlos manualmente.") },
      ],
    },
    {
      id: "mod-2-1", cat: t("Cash Control", "Control de Caja"), icon: "calculator",
      title: t("How Cash Flow Works", "Cómo Funciona el Flujo de Caja"), level: t("Intermediate", "Intermedio"), mins: 15,
      desc: t("Complete explanation of the nightly cash flow from opening to closing.", "Explicación completa del flujo de efectivo nocturno desde apertura hasta cierre."),
      steps: [
        { title: t("The master formula", "La fórmula maestra"), body: t("Cash Expected at Close = Opening Balance + Net Sales (cash) + House Fee (dancers) + Cover/Parking (cash) − Expenses − Staff Payments − Singles Exchange", "Cash Esperado al Cierre = Saldo de Apertura + Net Sales (efectivo) + House Fee (bailarinas) + Cover/Parking (efectivo) − Gastos − Pagos a Staff − Cambio de Singles"), rule: t("The system calculates this automatically. You only enter the raw inputs. Never edit the totals.", "El sistema calcula esto automáticamente. Solo ingresas los datos brutos. Nunca edites los totales.") },
        { title: t("What comes from Toast", "Qué viene de Toast"), body: t("Toast provides: Net Sales, Service Charge, Tips (card). These are imported or manually entered from the Toast close report at end of night.", "Toast provee: Net Sales, Service Charge, Propinas (tarjeta). Estos se importan o ingresan manualmente del reporte de cierre de Toast al final de la noche.") },
        { title: t("What is entered manually", "Qué se ingresa manualmente"), body: t("Manual entries: Cash from door (cover + parking), House Fee from dancers, Expenses, Singles exchange amounts, Staff payments (promoters, housters).", "Entradas manuales: Efectivo de puerta (cover + parking), House Fee de bailarinas, Gastos, Montos de cambio de singles, Pagos a staff (promotores, housters).") },
        { title: t("The closing reconciliation", "La reconciliación de cierre"), body: t("At end of night: count both safes physically. Enter the counted amounts. The system compares to the expected amount and shows: BALANCED, SHORT (missing cash), or CREDIT (extra cash).", "Al final de la noche: cuenta físicamente ambas cajas. Ingresa los montos contados. El sistema compara con el monto esperado y muestra: CUADRADO, SHORT (falta efectivo), o CREDIT (sobra efectivo).") },
      ],
    },
    {
      id: "mod-2-2", cat: t("Cash Control", "Control de Caja"), icon: "receipt",
      title: t("Registering Expenses", "Registro de Gastos"), level: t("Basic", "Básico"), mins: 5,
      desc: t("How to log every nightly cash expense with concept, amount, and receipt reference.", "Cómo registrar cada gasto nocturno en efectivo con concepto, monto y referencia de recibo."),
      steps: [
        { title: t("Why expenses must be logged", "Por qué los gastos deben registrarse"), body: t("Unregistered expenses are the #1 cause of safe discrepancies. If the manager spent $200 on ice and didn't log it, the system will show a $200 short at closing.", "Los gastos no registrados son la causa #1 de descuadres de caja. Si el manager gastó $200 en hielo y no lo registró, el sistema mostrará un faltante de $200 al cierre.") },
        { title: t("How to log an expense", "Cómo registrar un gasto"), body: t("Go to Cash Control → Expenses → Add Expense. Fill in: Concept (what was bought), Amount, Receipt reference (number or description). Save.", "Ve a Cash Control → Gastos → Agregar Gasto. Llena: Concepto (qué se compró), Monto, Referencia de recibo (número o descripción). Guarda.") },
        { title: t("The physical receipt", "El recibo físico"), body: t("Every expense must have a physical receipt. Place it in the yellow envelope in the office. The receipt reference in the system must match the envelope.", "Todo gasto debe tener un recibo físico. Colócalo en el sobre amarillo en la oficina. La referencia del recibo en el sistema debe coincidir con el sobre."), rule: t("No receipt = expense is not valid. The manager is responsible for collecting receipts for every cash purchase.", "Sin recibo = el gasto no es válido. El manager es responsable de recolectar recibos de toda compra en efectivo.") },
        { title: t("Expenses reduce expected cash", "Los gastos reducen el efectivo esperado"), body: t("Once logged, the expense is automatically subtracted from the Cash Expected at Close formula. This is why the safe will balance correctly.", "Una vez registrado, el gasto se resta automáticamente de la fórmula de Cash Esperado al Cierre. Por eso la caja cuadrará correctamente.") },
      ],
    },
    {
      id: "mod-2-3", cat: t("Cash Control", "Control de Caja"), icon: "alert-triangle",
      title: t("Shorts and Credits", "Shorts y Credits"), level: t("Intermediate", "Intermedio"), mins: 8,
      desc: t("What to do when the safe doesn't balance — registering shortfalls and overages.", "Qué hacer cuando la caja no cuadra — registrando faltantes y sobrantes."),
      steps: [
        { title: t("SHORT: missing cash", "SHORT: efectivo faltante"), body: t("A SHORT occurs when the physical cash counted is LESS than what the system expected. Example: system expects $15,000, you count $14,800 → SHORT of $200.", "Un SHORT ocurre cuando el efectivo físico contado es MENOR a lo que el sistema esperaba. Ejemplo: el sistema espera $15,000, cuentas $14,800 → SHORT de $200.") },
        { title: t("CREDIT: extra cash", "CREDIT: efectivo sobrante"), body: t("A CREDIT occurs when the physical cash counted is MORE than expected. Example: system expects $15,000, you count $15,200 → CREDIT of $200.", "Un CREDIT ocurre cuando el efectivo físico contado es MÁS de lo esperado. Ejemplo: el sistema espera $15,000, cuentas $15,200 → CREDIT de $200.") },
        { title: t("How to register a Short or Credit", "Cómo registrar un Short o Credit"), body: t("After entering the physical count, if there's a difference, the system shows a dialog: 'Difference detected: -$200. Register as SHORT?' Enter a mandatory explanation note. The record is permanent and cannot be deleted.", "Después de ingresar el conteo físico, si hay diferencia, el sistema muestra un diálogo: 'Diferencia detectada: -$200. ¿Registrar como SHORT?' Ingresa una nota de explicación obligatoria. El registro es permanente y no puede borrarse."), rule: t("Before registering a Short, always check: (1) Are all expenses logged? (2) Is the singles exchange registered? (3) Check the Audit Trail for unauthorized changes.", "Antes de registrar un Short, siempre verifica: (1) ¿Están todos los gastos registrados? (2) ¿Está registrado el cambio de singles? (3) Revisa el Audit Trail por cambios no autorizados.") },
        { title: t("Investigating discrepancies", "Investigando discrepancias"), body: t("Go to Admin Portal → Audit Trail. Filter by date. Look for any manual changes to calculated fields. If someone changed a formula result manually, it will appear here with their username and timestamp.", "Ve al Portal Admin → Audit Trail. Filtra por fecha. Busca cualquier cambio manual a campos calculados. Si alguien cambió un resultado de fórmula manualmente, aparecerá aquí con su nombre de usuario y hora.") },
      ],
    },
    {
      id: "mod-2-4", cat: t("Cash Control", "Control de Caja"), icon: "banknote",
      title: t("Singles Exchange", "Cambio de Billetes (Singles)"), level: t("Basic", "Básico"), mins: 3,
      desc: t("How to register when cash is taken from the big safe to exchange for $1 bills.", "Cómo registrar cuando se saca efectivo del big safe para cambiar por billetes de $1."),
      steps: [
        { title: t("What is a singles exchange", "Qué es un cambio de singles"), body: t("Singles ($1 bills) are used by customers to tip dancers. When the club runs low on singles, the manager takes larger bills from the Big Safe to a currency exchange. This cash must be tracked.", "Los singles (billetes de $1) son usados por clientes para propinas a las bailarinas. Cuando el club se queda sin singles, el manager lleva billetes más grandes del Big Safe a cambio de moneda. Este efectivo debe rastrearse.") },
        { title: t("Register the exchange", "Registra el cambio"), body: t("Go to Cash Control → Internal Movements → Singles Exchange. Enter: Amount taken from Big Safe, Date/Time. The system records this as an internal movement — it doesn't reduce the total, it just tracks the flow.", "Ve a Cash Control → Movimientos Internos → Cambio de Singles. Ingresa: Monto sacado del Big Safe, Fecha/Hora. El sistema lo registra como movimiento interno — no reduce el total, solo rastrea el flujo."), rule: t("If you take $10,000 for singles and don't register it, the system will show a $10,000 short at closing.", "Si sacas $10,000 para singles y no lo registras, el sistema mostrará un faltante de $10,000 al cierre.") },
      ],
    },
    {
      id: "mod-3-1", cat: t("System Formulas", "Fórmulas del Sistema"), icon: "trending-up",
      title: t("Gross vs Net Sales", "Gross vs Net Sales"), level: t("Intermediate", "Intermedio"), mins: 10,
      desc: t("The critical difference between gross and net sales, and why it matters for cash reconciliation.", "La diferencia crítica entre ventas brutas y netas, y por qué importa para la reconciliación de caja."),
      steps: [
        { title: t("Gross Sales — what Toast shows", "Gross Sales — lo que muestra Toast"), body: t("Gross Sales is the total of ALL payments processed through Toast. It includes: food & drink sales + Service Charge + Tips (card) + Tax. This is the big number at the top of the Toast report.", "Gross Sales es el total de TODOS los pagos procesados por Toast. Incluye: ventas de comida y bebida + Service Charge + Propinas (tarjeta) + Impuesto. Es el número grande en la parte superior del reporte de Toast.") },
        { title: t("Net Sales — what the system uses", "Net Sales — lo que usa el sistema"), body: t("Net Sales = Gross Sales − Tips − Tax − Service Charge. This is the actual revenue from food and drinks only. The system uses Net Sales for all internal calculations.", "Net Sales = Gross Sales − Propinas − Impuesto − Service Charge. Este es el ingreso real solo de comida y bebidas. El sistema usa Net Sales para todos los cálculos internos."), rule: t("NEVER use Gross Sales as your revenue number. Always use Net Sales for reconciliation.", "NUNCA uses Gross Sales como tu número de ingresos. Siempre usa Net Sales para reconciliación.") },
        { title: t("Finding Net Sales in Toast", "Encontrar Net Sales en Toast"), body: t("In Toast: go to Reports → End of Day → Payment Summary. Scroll to the bottom right. The total shown there is Gross. To get Net: subtract the Tips line and the Service Charge line and the Tax line.", "En Toast: ve a Reports → End of Day → Payment Summary. Baja hasta la esquina inferior derecha. El total mostrado es Gross. Para obtener Net: resta la línea de Tips, la línea de Service Charge y la línea de Tax.") },
        { title: t("Cash vs card split", "División efectivo vs tarjeta"), body: t("Of the Net Sales, some was paid in cash and some by card. The system needs only the CASH portion of Net Sales for the safe reconciliation. Card payments stay in Toast — they don't affect the physical safe.", "Del Net Sales, parte fue pagado en efectivo y parte con tarjeta. El sistema necesita solo la porción EN EFECTIVO del Net Sales para la reconciliación de caja. Los pagos con tarjeta se quedan en Toast — no afectan la caja física.") },
      ],
    },
    {
      id: "mod-3-2", cat: t("System Formulas", "Fórmulas del Sistema"), icon: "percent",
      title: t("House Fee by Role", "House Fee por Rol"), level: t("Intermediate", "Intermedio"), mins: 8,
      desc: t("Why servers pay 17% and bartenders 13%, and how the system calculates it automatically.", "Por qué los servers pagan 17% y los bartenders 13%, y cómo el sistema lo calcula automáticamente."),
      steps: [
        { title: t("What is the House percentage", "Qué es el porcentaje de casa"), body: t("The House percentage is the portion of each employee's sales that goes to the club (the house). It's deducted from the employee's earnings and added to the club's revenue.", "El porcentaje de casa es la porción de las ventas de cada empleado que va al club (la casa). Se deduce de las ganancias del empleado y se suma a los ingresos del club.") },
        { title: t("Server rate: 17%", "Tasa para servers: 17%"), body: t("Servers (waitresses/waiters) contribute 17% of their total sales to the house. Example: a server with $3,000 in sales → House portion = $510.", "Los servers (meseras/meseros) contribuyen el 17% de sus ventas totales a la casa. Ejemplo: un server con $3,000 en ventas → Porción de casa = $510."), rule: t("This percentage is LOCKED in the system. No manager or staff can change it manually.", "Este porcentaje está BLOQUEADO en el sistema. Ningún manager o empleado puede cambiarlo manualmente.") },
        { title: t("Bartender rate: 13%", "Tasa para bartenders: 13%"), body: t("Bartenders contribute 13% of their total sales to the house. Example: a bartender with $5,000 in sales → House portion = $650.", "Los bartenders contribuyen el 13% de sus ventas totales a la casa. Ejemplo: un bartender con $5,000 en ventas → Porción de casa = $650.") },
        { title: t("Where this appears in the system", "Dónde aparece esto en el sistema"), body: t("In the Employee Close Report, Line 'House Retention' shows the calculated amount. This is subtracted from the employee's gross earnings automatically. The employee receives the remainder.", "En el Close Report del empleado, la línea 'Retención de Casa' muestra el monto calculado. Esto se resta automáticamente de las ganancias brutas del empleado. El empleado recibe el resto.") },
      ],
    },
    {
      id: "mod-3-3", cat: t("System Formulas", "Fórmulas del Sistema"), icon: "function-square",
      title: t("The Complete Close Formula", "Fórmula de Cierre Completa"), level: t("Advanced", "Avanzado"), mins: 15,
      desc: t("The full equation for Cash Expected at Close — every variable explained.", "La ecuación completa del Cash Esperado al Cierre — cada variable explicada."),
      steps: [
        { title: t("The formula", "La fórmula"), body: t("Cash Expected at Close = Opening Balance (Big Safe + Small Safe) + Net Sales in Cash + House Fee Total (dancers) + Cover/Parking in Cash − Total Expenses − Staff Payments (promoters, housters) − Singles Exchange Amount", "Cash Esperado al Cierre = Saldo de Apertura (Big Safe + Small Safe) + Net Sales en Efectivo + House Fee Total (bailarinas) + Cover/Parking en Efectivo − Total de Gastos − Pagos a Staff (promotores, housters) − Monto de Cambio de Singles") },
        { title: t("Opening Balance", "Saldo de Apertura"), body: t("This is the sum of Big Safe + Small Safe at the start of the day. It must match the previous night's closing balance. The system carries it forward automatically.", "Es la suma del Big Safe + Small Safe al inicio del día. Debe coincidir con el saldo de cierre de la noche anterior. El sistema lo arrastra automáticamente.") },
        { title: t("Net Sales in Cash", "Net Sales en Efectivo"), body: t("Only the cash portion of Net Sales. Obtained from Toast's End of Day report: look for 'Cash' under Payment Methods. Do NOT include card payments.", "Solo la porción en efectivo del Net Sales. Se obtiene del reporte End of Day de Toast: busca 'Cash' bajo Métodos de Pago. NO incluyas pagos con tarjeta.") },
        { title: t("What gets subtracted", "Qué se resta"), body: t("Three things reduce the expected cash: (1) Expenses — every cash purchase during the night. (2) Staff payments — promoters, housters paid in cash. (3) Singles exchange — cash taken for $1 bill exchange.", "Tres cosas reducen el efectivo esperado: (1) Gastos — toda compra en efectivo durante la noche. (2) Pagos a staff — promotores, housters pagados en efectivo. (3) Cambio de singles — efectivo sacado para cambio de billetes de $1."), rule: t("If the safe shows a short and you can't explain it — check these three items first. 90% of discrepancies come from unregistered expenses or singles exchanges.", "Si la caja muestra un faltante y no puedes explicarlo — revisa estos tres ítems primero. El 90% de las discrepancias vienen de gastos o cambios de singles no registrados.") },
      ],
    },
    {
      id: "mod-3-4", cat: t("System Formulas", "Fórmulas del Sistema"), icon: "plug",
      title: t("Toast Integration", "Integración con Toast"), level: t("Advanced", "Avanzado"), mins: 12,
      desc: t("What data comes from Toast POS and what must be entered manually in the system.", "Qué datos vienen del POS Toast y cuáles deben ingresarse manualmente en el sistema."),
      steps: [
        { title: t("Toast is the source of truth for sales", "Toast es la fuente de verdad para ventas"), body: t("All food and beverage sales, service charges, and card tips are recorded in Toast. The PinkPony system does NOT replace Toast — it uses Toast data as input.", "Todas las ventas de comida y bebida, service charges y propinas de tarjeta se registran en Toast. El sistema PinkPony NO reemplaza a Toast — usa los datos de Toast como entrada.") },
        { title: t("Data that comes FROM Toast", "Datos que vienen DE Toast"), body: t("From Toast End of Day report: Net Sales (total), Net Sales in Cash, Net Sales by Card, Service Charge total, Tips (card) total, Tax total.", "Del reporte End of Day de Toast: Net Sales (total), Net Sales en Efectivo, Net Sales por Tarjeta, Service Charge total, Propinas (tarjeta) total, Impuesto total.") },
        { title: t("Data entered MANUALLY in the system", "Datos ingresados MANUALMENTE en el sistema"), body: t("Manual entries: House Fee from dancers, Cover/Parking cash, Expenses, Singles exchange, Staff payments, Safe opening counts, Safe closing counts.", "Entradas manuales: House Fee de bailarinas, Cover/Parking en efectivo, Gastos, Cambio de singles, Pagos a staff, Conteos de apertura de caja, Conteos de cierre de caja.") },
        { title: t("The reconciliation point", "El punto de reconciliación"), body: t("The system reconciles when: Toast data (Net Sales cash) + Manual data (House Fee + Cover cash − Expenses − Payments − Singles) = Physical safe count. If these match → BALANCED.", "El sistema reconcilia cuando: Datos de Toast (Net Sales efectivo) + Datos manuales (House Fee + Cover efectivo − Gastos − Pagos − Singles) = Conteo físico de caja. Si coinciden → CUADRADO.") },
      ],
    },
    {
      id: "mod-4-1", cat: t("Admin & Reports", "Administración y Reportes"), icon: "layout-dashboard",
      title: t("Executive Dashboard", "Dashboard Ejecutivo"), level: t("Basic", "Básico"), mins: 5,
      desc: t("How to read the daily summary as owner or administrator — no editing required.", "Cómo leer el resumen diario como dueño o administrador — sin necesidad de editar."),
      steps: [
        { title: t("What the dashboard shows", "Qué muestra el dashboard"), body: t("The Executive Dashboard is a read-only view for owners. It shows: Net Sales of the day, House Fee total, Total Expenses, Cash Expected vs Cash Counted, and the Difference (Short/Credit/Balanced).", "El Dashboard Ejecutivo es una vista de solo lectura para dueños. Muestra: Net Sales del día, House Fee total, Total de Gastos, Cash Esperado vs Cash Contado, y la Diferencia (Short/Credit/Cuadrado).") },
        { title: t("Green means balanced", "Verde significa cuadrado"), body: t("When the difference is $0, the dashboard shows a green BALANCED indicator. Any other color means there's a discrepancy that needs investigation.", "Cuando la diferencia es $0, el dashboard muestra un indicador verde CUADRADO. Cualquier otro color significa que hay una discrepancia que necesita investigación.") },
        { title: t("Drilling down", "Profundizando"), body: t("Click any number on the dashboard to see the breakdown. Click 'Expenses' to see each individual expense logged that night. Click 'House Fee' to see each dancer's contribution.", "Haz click en cualquier número del dashboard para ver el desglose. Click en 'Gastos' para ver cada gasto individual registrado esa noche. Click en 'House Fee' para ver la contribución de cada bailarina.") },
      ],
    },
    {
      id: "mod-4-2", cat: t("Admin & Reports", "Administración y Reportes"), icon: "history",
      title: t("Audit Trail", "Audit Trail"), level: t("Intermediate", "Intermedio"), mins: 8,
      desc: t("How to review the change history and detect unauthorized modifications.", "Cómo revisar el historial de cambios y detectar modificaciones no autorizadas."),
      steps: [
        { title: t("What the Audit Trail records", "Qué registra el Audit Trail"), body: t("Every manual change to any number in the system creates an immutable record: User who made the change, Timestamp, Field that was changed, Previous value, New value. This cannot be deleted or modified.", "Cada cambio manual a cualquier número en el sistema crea un registro inmutable: Usuario que hizo el cambio, Hora, Campo que se cambió, Valor anterior, Valor nuevo. Esto no puede borrarse ni modificarse.") },
        { title: t("How to access it", "Cómo acceder"), body: t("Go to Admin Portal → Audit Trail. Filter by date range, by user, or by field type. Look for any changes to calculated fields — these are red flags.", "Ve al Portal Admin → Audit Trail. Filtra por rango de fechas, por usuario, o por tipo de campo. Busca cualquier cambio a campos calculados — estas son señales de alerta.") },
        { title: t("Red flags to watch for", "Señales de alerta a vigilar"), body: t("Warning signs: (1) A calculated total was manually overridden. (2) A formula result was changed to match a desired number. (3) Multiple changes to the same field in one night. (4) Changes made after closing time.", "Señales de advertencia: (1) Un total calculado fue sobreescrito manualmente. (2) Un resultado de fórmula fue cambiado para coincidir con un número deseado. (3) Múltiples cambios al mismo campo en una noche. (4) Cambios hechos después del horario de cierre."), rule: t("Calculated fields are locked — staff cannot edit them. If the Audit Trail shows a change to a calculated field, escalate immediately.", "Los campos calculados están bloqueados — el staff no puede editarlos. Si el Audit Trail muestra un cambio a un campo calculado, escala inmediatamente.") },
      ],
    },
    {
      id: "mod-4-3", cat: t("Admin & Reports", "Administración y Reportes"), icon: "file-bar-chart",
      title: t("Dancer Daily Report", "Reporte Diario de Bailarinas"), level: t("Basic", "Básico"), mins: 5,
      desc: t("How to read and verify the daily dancer report for House Fee accuracy.", "Cómo leer y verificar el reporte diario de bailarinas para exactitud del House Fee."),
      steps: [
        { title: t("Accessing the report", "Acceder al reporte"), body: t("Go to Admin Portal → Dancers → Daily Report. Select the date. The report shows each dancer who checked in that night.", "Ve al Portal Admin → Dancers → Reporte Diario. Selecciona la fecha. El reporte muestra cada bailarina que hizo check-in esa noche.") },
        { title: t("What the report shows", "Qué muestra el reporte"), body: t("For each dancer: Name, Check-in time, Number of private dances registered, Entry fee paid, Dance fees total, Total House Fee contributed.", "Por cada bailarina: Nombre, Hora de check-in, Número de bailes privados registrados, Tarifa de entrada pagada, Total de tarifas de baile, Total de House Fee contribuido.") },
        { title: t("Verifying the total", "Verificar el total"), body: t("The sum of all dancers' House Fee at the bottom of the report must match the 'House Fee' line in the Cash Control module. If they don't match, a dancer's data is missing or incorrect.", "La suma del House Fee de todas las bailarinas al final del reporte debe coincidir con la línea 'House Fee' en el módulo de Cash Control. Si no coinciden, faltan datos de una bailarina o son incorrectos.") },
      ],
    },
    {
      id: "mod-4-4", cat: t("Admin & Reports", "Administración y Reportes"), icon: "shield",
      title: t("Roles and Permissions", "Roles y Permisos"), level: t("Basic", "Básico"), mins: 5,
      desc: t("What each role can and cannot do in the system.", "Qué puede y no puede hacer cada rol en el sistema."),
      steps: [
        { title: t("Administrator (Owner)", "Administrador (Dueño)"), body: t("Full access: read all reports, view Audit Trail, access Executive Dashboard, configure system settings. Cannot be locked out. This is the owner's role.", "Acceso completo: leer todos los reportes, ver Audit Trail, acceder al Dashboard Ejecutivo, configurar ajustes del sistema. No puede ser bloqueado. Este es el rol del dueño.") },
        { title: t("Manager", "Manager"), body: t("Can: open/close shifts, register expenses, confirm employee close reports, enter safe counts, register shorts/credits. Cannot: edit calculated fields, delete Audit Trail entries, change house percentages.", "Puede: abrir/cerrar turnos, registrar gastos, confirmar close reports de empleados, ingresar conteos de caja, registrar shorts/credits. No puede: editar campos calculados, borrar entradas del Audit Trail, cambiar porcentajes de casa.") },
        { title: t("Door Staff", "Staff de Puerta"), body: t("Can: register covers, parking, and entries (cash and card). Cannot: access Cash Control, view safe balances, or modify any other module.", "Puede: registrar covers, parking y entradas (efectivo y tarjeta). No puede: acceder a Cash Control, ver saldos de caja, ni modificar ningún otro módulo.") },
        { title: t("Dancer", "Bailarina"), body: t("Can: check in, register dances, view own earnings. Cannot: access any management module, view other dancers' data, or modify any system data.", "Puede: hacer check-in, registrar bailes, ver sus propias ganancias. No puede: acceder a ningún módulo de gestión, ver datos de otras bailarinas, ni modificar ningún dato del sistema.") },
      ],
    },
  ];

  const VIDEOS = [
    { id: "v1", title: t("Complete System Tour", "Tour Completo del Sistema"), mins: 12, desc: t("Full walkthrough of all Admin Portal modules.", "Recorrido completo de todos los módulos del Portal Admin."), transcript: t("In this video we walk through every section of the PinkPony Admin Portal starting with the Dashboard...", "En este video recorremos cada sección del Portal Admin de PinkPony comenzando con el Dashboard...") },
    { id: "v2", title: t("Closing the Safe Correctly", "Cerrar la Caja Correctamente"), mins: 8, desc: t("Step-by-step nightly close process.", "Proceso de cierre nocturno paso a paso."), transcript: t("The nightly close starts with counting both safes physically before entering any numbers...", "El cierre nocturno comienza contando físicamente ambas cajas antes de ingresar cualquier número...") },
    { id: "v3", title: t("Dancer Check-In in 3 Minutes", "Check-In de Bailarinas en 3 Minutos"), mins: 3, desc: t("Quick tutorial for the dancer registration flow.", "Tutorial rápido del flujo de registro de bailarinas."), transcript: t("Open the Dancers module, click Check In, select the dancer name, confirm entry fee...", "Abre el módulo Dancers, haz click en Check In, selecciona el nombre de la bailarina, confirma la tarifa de entrada...") },
    { id: "v4", title: t("Understanding Cash Control", "Entendiendo el Cash Control"), mins: 10, desc: t("Visual explanation of the nightly cash flow.", "Explicación visual del flujo de efectivo nocturno."), transcript: t("Cash Control is the heart of the system. Every dollar that enters or leaves the club must be tracked here...", "Cash Control es el corazón del sistema. Cada dólar que entra o sale del club debe rastrearse aquí...") },
    { id: "v5", title: t("Reading the Executive Dashboard", "Leyendo el Dashboard Ejecutivo"), mins: 5, desc: t("For owners and administrators — read-only overview.", "Para dueños y administradores — vista general de solo lectura."), transcript: t("The Executive Dashboard gives you a one-screen summary of the night's performance...", "El Dashboard Ejecutivo te da un resumen en una pantalla del desempeño de la noche...") },
    { id: "v6", title: t("Audit Trail: Detecting Errors", "Audit Trail: Detectando Errores"), mins: 7, desc: t("How to use the change history to find unauthorized modifications.", "Cómo usar el historial de cambios para encontrar modificaciones no autorizadas."), transcript: t("The Audit Trail is your security net. Every change is logged with user, time, and values...", "El Audit Trail es tu red de seguridad. Cada cambio se registra con usuario, hora y valores...") },
  ];

  const CHECKLISTS = [
    { id: "cl1", icon: "sunrise", title: t("Shift Opening", "Apertura de Turno"), role: t("Manager", "Manager"), items: [
      t("Count Big Safe and enter amount in system", "Contar Big Safe e ingresar monto en sistema"),
      t("Count Small Safe and enter amount in system", "Contar Small Safe e ingresar monto en sistema"),
      t("Verify total matches previous night's close", "Verificar que el total coincide con el cierre de anoche"),
      t("If difference exists, register Short or Credit with note", "Si hay diferencia, registrar Short o Credit con nota"),
      t("Confirm employee roster for tonight", "Confirmar lista de empleados para esta noche"),
      t("Verify all dancers have checked in", "Verificar que todas las bailarinas hicieron check-in"),
      t("Review confirmed reservations for tonight", "Revisar reservaciones confirmadas para esta noche"),
    ] },
    { id: "cl2", icon: "moon", title: t("During the Night", "Durante la Noche"), role: t("Manager", "Manager"), items: [
      t("Log all cash expenses in Expenses module with receipt", "Registrar todos los gastos en efectivo en módulo de Gastos con recibo"),
      t("If singles exchange done, register as internal movement", "Si se hizo cambio de singles, registrar como movimiento interno"),
      t("Verify door staff is logging covers and parking", "Verificar que el staff de puerta está registrando covers y parking"),
      t("Check that dancer dances are being registered in real time", "Verificar que los bailes de bailarinas se registran en tiempo real"),
      t("Monitor Cash Control dashboard for unexpected variances", "Monitorear dashboard de Cash Control por variaciones inesperadas"),
    ] },
    { id: "cl3", icon: "sunset", title: t("Shift Close", "Cierre de Turno"), role: t("Manager", "Manager"), items: [
      t("Generate Close Report for each Server", "Generar Close Report para cada Server"),
      t("Generate Close Report for each Bartender", "Generar Close Report para cada Bartender"),
      t("Confirm Cash Drop from each employee", "Confirmar Cash Drop de cada empleado"),
      t("Count Big Safe physically", "Contar Big Safe físicamente"),
      t("Count Small Safe physically", "Contar Small Safe físicamente"),
      t("Enter physical counts in system", "Ingresar conteos físicos en sistema"),
      t("Verify system shows BALANCED or register Short/Credit with note", "Verificar que sistema muestra CUADRADO o registrar Short/Credit con nota"),
      t("Review dancer daily report", "Revisar reporte diario de bailarinas"),
      t("Close the day in the system", "Cerrar el día en el sistema"),
    ] },
    { id: "cl4", icon: "shield-check", title: t("QA Validation", "Validación QA"), role: t("Admin", "Admin"), items: [
      t("Verify calculated fields are not editable (try to modify a total)", "Verificar que campos calculados no son editables (intentar modificar un total)"),
      t("Run a test close with known numbers and verify it balances", "Hacer un cierre de prueba con números conocidos y verificar que cuadra"),
      t("Run a test close with intentional Short and verify system records it", "Hacer un cierre de prueba con Short intencional y verificar que el sistema lo registra"),
      t("Verify Audit Trail shows all manual changes", "Verificar que el Audit Trail muestra todos los cambios manuales"),
      t("Verify previous night's balance appears as today's opening", "Verificar que el saldo de anoche aparece como apertura de hoy"),
      t("Verify dancer House Fee feeds correctly into Cash Control", "Verificar que el House Fee de bailarinas alimenta correctamente el Cash Control"),
      t("Confirm with the Manager that numbers match Toast", "Confirmar con el Manager que los números coinciden con Toast"),
    ] },
  ];

  const GLOSSARY = [
    { term: "Audit Trail", def: t("Immutable record of all manual changes in the system. Shows who changed what, when, and from which value to which value. Cannot be deleted.", "Registro inmutable de todos los cambios manuales en el sistema. Muestra quién cambió qué, cuándo y de qué valor a qué valor. No puede borrarse.") },
    { term: "Big Safe", def: t("Large vault where the majority of the club's cash is stored. Must be counted at opening and closing every day.", "Caja fuerte grande donde se guarda la mayor parte del efectivo del club. Debe contarse en apertura y cierre todos los días.") },
    { term: t("Bartender House %", "Bartender House %"), def: t("13% of each bartender's total sales retained by the house. Calculated automatically by the system based on employee role.", "13% de las ventas totales de cada bartender retenido por la casa. Calculado automáticamente por el sistema según el rol del empleado.") },
    { term: "Cash Drop", def: t("The cash amount each employee must hand over to the Manager at end of shift. Calculated automatically in the Close Report.", "El monto de efectivo que cada empleado debe entregar al Manager al final del turno. Calculado automáticamente en el Close Report.") },
    { term: t("Cash Expected at Close", "Cash Esperado al Cierre"), def: t("System formula: Opening Balance + Net Sales Cash + House Fee + Cover/Parking Cash − Expenses − Staff Payments − Singles Exchange.", "Fórmula del sistema: Saldo de Apertura + Net Sales Efectivo + House Fee + Cover/Parking Efectivo − Gastos − Pagos Staff − Cambio Singles.") },
    { term: "Close Report", def: t("Per-employee summary generated at end of shift showing: total sales, cash drop expected, tips, service charge, and house percentage.", "Resumen por empleado generado al final del turno mostrando: ventas totales, cash drop esperado, propinas, service charge y porcentaje de casa.") },
    { term: "Cover", def: t("Entry fee charged to each customer entering the club. Can be cash or card. Cash covers feed the safe; card covers go through Toast.", "Cargo de entrada cobrado a cada cliente que entra al club. Puede ser efectivo o tarjeta. Los covers en efectivo alimentan la caja; los de tarjeta van por Toast.") },
    { term: "Credit", def: t("Overage at closing. When physical cash counted is MORE than what the system expected. Must be registered with a mandatory explanation note.", "Sobrante al cierre. Cuando el efectivo físico contado es MÁS de lo que el sistema esperaba. Debe registrarse con nota de explicación obligatoria.") },
    { term: t("Expenses / Gastos", "Expenses / Gastos"), def: t("Operational cash expenses during the night (ice, emergency liquor, supplies). Must be logged with concept, amount, and receipt reference. Automatically subtracted from expected cash.", "Gastos operativos en efectivo durante la noche (hielo, licor de emergencia, suministros). Deben registrarse con concepto, monto y referencia de recibo. Se restan automáticamente del efectivo esperado.") },
    { term: "Gross Sales", def: t("Total of ALL payments processed through Toast. Includes: food & drink sales + Service Charge + Tips (card) + Tax. NOT used for reconciliation — use Net Sales instead.", "Total de TODOS los pagos procesados por Toast. Incluye: ventas de comida y bebida + Service Charge + Propinas (tarjeta) + Impuesto. NO se usa para reconciliación — usa Net Sales en su lugar.") },
    { term: "House Fee", def: t("Amount paid by dancers to the club for each private dance and for their entry. Feeds directly into the nightly cash total.", "Monto pagado por las bailarinas al club por cada baile privado y por su entrada. Alimenta directamente el total de efectivo nocturno.") },
    { term: "House Portion", def: t("Percentage of Service Charge retained by the house, contributed by servers (17%) and bartenders (13%).", "Porcentaje del Service Charge retenido por la casa, contribuido por servers (17%) y bartenders (13%).") },
    { term: "Net Sales", def: t("Actual revenue from food and drinks only. Formula: Gross Sales − Tips − Tax − Service Charge. Used for all internal reconciliation calculations.", "Ingreso real solo de comida y bebidas. Fórmula: Gross Sales − Propinas − Impuesto − Service Charge. Usado para todos los cálculos internos de reconciliación.") },
    { term: "Overtip", def: t("Additional tip given directly by a customer to an employee, above the automatic Service Charge. Recorded separately in the system.", "Propina adicional dada directamente por un cliente a un empleado, por encima del Service Charge automático. Se registra por separado en el sistema.") },
    { term: t("Private Room", "Cuarto Privado (VIP)"), def: t("Private room where private dances occur. House Fee from these dances feeds the safe. Must be registered in the system immediately after each dance.", "Cuarto privado donde ocurren los bailes privados. El House Fee de estos bailes alimenta la caja. Debe registrarse en el sistema inmediatamente después de cada baile.") },
    { term: t("Server House %", "Server House %"), def: t("17% of each server's total sales retained by the house. Calculated automatically by the system based on employee role. Locked — cannot be changed manually.", "17% de las ventas totales de cada server retenido por la casa. Calculado automáticamente por el sistema según el rol del empleado. Bloqueado — no puede cambiarse manualmente.") },
    { term: "Service Charge", def: t("Automatic service fee included in each customer's bill. Distributed between staff and the house according to role percentages.", "Cargo de servicio automático incluido en la cuenta de cada cliente. Distribuido entre el staff y la casa según los porcentajes por rol.") },
    { term: "Short", def: t("Shortfall at closing. When physical cash counted is LESS than what the system expected. Must be registered with a mandatory explanation note. Permanent record.", "Faltante al cierre. Cuando el efectivo físico contado es MENOS de lo que el sistema esperaba. Debe registrarse con nota de explicación obligatoria. Registro permanente.") },
    { term: "Singles", def: t("$1 bills used by customers to tip dancers. Exchanged from the Big Safe. Exchange must be registered as an internal movement to avoid showing as a short.", "Billetes de $1 usados por clientes para propinas a bailarinas. Se cambian del Big Safe. El cambio debe registrarse como movimiento interno para evitar que aparezca como un faltante.") },
    { term: "Small Safe", def: t("Small vault (petty cash box) for daily operational cash. Used for small expenses and change. Must be counted at opening and closing every day.", "Caja fuerte pequeña (caja chica) para efectivo operativo diario. Usada para gastos pequeños y cambio. Debe contarse en apertura y cierre todos los días.") },
    { term: "Toast", def: t("The club's POS (Point of Sale) system. Official source of Net Sales, Service Charge, and card Tips. PinkPony system uses Toast data as input for reconciliation.", "El sistema POS (Punto de Venta) del club. Fuente oficial de Net Sales, Service Charge y Propinas de tarjeta. El sistema PinkPony usa los datos de Toast como entrada para la reconciliación.") },
  ];

  const FAQ = [
    { g: t("Safe & Cash", "Caja y Efectivo"), q: t("Why doesn't the safe balance even though the numbers look right?", "¿Por qué la caja no cuadra si los números se ven correctos?"), a: t("The most common cause is unregistered expenses. If the manager spent cash during the night and didn't log it in the Expenses module, the system doesn't know that money left. Check Expenses first, then check the Singles Exchange log.", "La causa más común son los gastos no registrados. Si el manager gastó efectivo durante la noche y no lo registró en el módulo de Gastos, el sistema no sabe que ese dinero salió. Revisa Gastos primero, luego revisa el registro de Cambio de Singles.") },
    { g: t("Safe & Cash", "Caja y Efectivo"), q: t("What do I do if the system shows a Short but I'm sure the money is there?", "¿Qué hago si el sistema muestra un Short pero estoy seguro de que el dinero está?"), a: t("First: verify all expenses are logged. Second: check if a singles exchange was done and registered. Third: open the Audit Trail and look for any manual changes to calculated fields that night. If everything checks out and the money is physically there, register a Credit with a detailed note.", "Primero: verifica que todos los gastos estén registrados. Segundo: verifica si se hizo un cambio de singles y si está registrado. Tercero: abre el Audit Trail y busca cambios manuales a campos calculados esa noche. Si todo está correcto y el dinero está físicamente, registra un Credit con nota detallada.") },
    { g: t("Safe & Cash", "Caja y Efectivo"), q: t("Why is the Toast total different from the Net Sales in the system?", "¿Por qué el total de Toast es diferente al Net Sales del sistema?"), a: t("Toast shows Gross Sales which includes Tips, Tax, and Service Charge. The system uses Net Sales which is Gross minus those three items. Always use the Payment Summary section of the Toast End of Day report and subtract Tips + Tax + Service Charge to get Net Sales.", "Toast muestra Gross Sales que incluye Propinas, Impuesto y Service Charge. El sistema usa Net Sales que es el Gross menos esos tres ítems. Siempre usa la sección Payment Summary del reporte End of Day de Toast y resta Propinas + Impuesto + Service Charge para obtener Net Sales.") },
    { g: t("Safe & Cash", "Caja y Efectivo"), q: t("Can I correct a number I entered incorrectly?", "¿Puedo corregir un número que ingresé mal?"), a: t("Yes, for input fields (not calculated fields). The system will record the correction in the Audit Trail with your username and the exact time. The original entry and the correction are both permanently visible.", "Sí, para campos de entrada (no campos calculados). El sistema registrará la corrección en el Audit Trail con tu nombre de usuario y la hora exacta. La entrada original y la corrección son ambas permanentemente visibles.") },
    { g: t("Safe & Cash", "Caja y Efectivo"), q: t("What if I forget to register an expense?", "¿Qué pasa si olvido registrar un gasto?"), a: t("The system will show a Short at closing equal to the unregistered expense amount. You can register the expense retroactively — it will be accepted — but the Audit Trail will show the actual time you entered it, which will be after closing. Always register expenses as they happen.", "El sistema mostrará un Short al cierre igual al monto del gasto no registrado. Puedes registrar el gasto retroactivamente — será aceptado — pero el Audit Trail mostrará la hora real en que lo ingresaste, que será después del cierre. Siempre registra los gastos cuando ocurren.") },
    { g: t("Dancers", "Bailarinas"), q: t("Why doesn't the dancer House Fee match what was reported?", "¿Por qué el House Fee de bailarinas no coincide con lo que se reportó?"), a: t("The system only counts dancers who checked in through the system and dances that were registered in real time. If any dancer didn't use the system, her House Fee is not counted. This is why 100% system adoption by all dancers is mandatory.", "El sistema solo cuenta bailarinas que hicieron check-in por el sistema y bailes registrados en tiempo real. Si alguna bailarina no usó el sistema, su House Fee no se cuenta. Por eso la adopción del 100% del sistema por todas las bailarinas es obligatoria.") },
    { g: t("Dancers", "Bailarinas"), q: t("Must all dancers use the system?", "¿Todas las bailarinas deben usar el sistema?"), a: t("Yes, without exception. The system is the only valid channel for registering dancer entries and dances. Without system registration, the House Fee is not counted and the safe will not balance.", "Sí, sin excepción. El sistema es el único canal válido para registrar entradas y bailes de bailarinas. Sin registro en el sistema, el House Fee no se cuenta y la caja no cuadrará.") },
    { g: t("Dancers", "Bailarinas"), q: t("How is a dancer's House Fee calculated?", "¿Cómo se calcula el House Fee de una bailarina?"), a: t("House Fee = Entry Fee + Sum of all private dance fees registered in the system for that dancer that night. The system calculates this automatically when all dances are properly registered.", "House Fee = Tarifa de Entrada + Suma de todas las tarifas de bailes privados registrados en el sistema para esa bailarina esa noche. El sistema calcula esto automáticamente cuando todos los bailes están correctamente registrados.") },
    { g: t("Employees", "Empleados"), q: t("Why is the house percentage different for servers and bartenders?", "¿Por qué el porcentaje de casa es diferente para servers y bartenders?"), a: t("Servers pay 17% and bartenders pay 13% because their sales structures and tip earnings are different. This is club policy and is fixed in the system — no one can change it manually, not even the manager.", "Los servers pagan 17% y los bartenders 13% porque sus estructuras de ventas y ganancias de propinas son diferentes. Esta es la política del club y está fija en el sistema — nadie puede cambiarla manualmente, ni siquiera el manager.") },
    { g: t("Employees", "Empleados"), q: t("What is the Cash Drop and how is it calculated?", "¿Qué es el Cash Drop y cómo se calcula?"), a: t("The Cash Drop is the cash each employee must hand to the Manager at end of shift. The system calculates it automatically in the Close Report: Cash Drop = Cash Sales − House % − Overtips retained. The employee receives the remainder after house deduction.", "El Cash Drop es el efectivo que cada empleado debe entregar al Manager al final del turno. El sistema lo calcula automáticamente en el Close Report: Cash Drop = Ventas en Efectivo − House % − Overtips retenidos. El empleado recibe el resto después de la deducción de casa.") },
    { g: t("Employees", "Empleados"), q: t("What if an employee hands over less cash than expected?", "¿Qué pasa si un empleado entrega menos efectivo del esperado?"), a: t("The Manager enters the actual amount handed over. The system marks it as a Short for that employee with a mandatory note. This record is permanent and visible to the Administrator.", "El Manager ingresa el monto real entregado. El sistema lo marca como un Short para ese empleado con nota obligatoria. Este registro es permanente y visible para el Administrador.") },
    { g: t("System", "Sistema"), q: t("Why can't I edit the totals?", "¿Por qué no puedo editar los totales?"), a: t("Calculated fields are locked to prevent manipulation. The system calculates totals from the raw inputs you enter. If a total seems wrong, the issue is in one of the inputs — find and correct the input, and the total will update automatically.", "Los campos calculados están bloqueados para prevenir manipulaciones. El sistema calcula los totales a partir de los datos brutos que ingresas. Si un total parece incorrecto, el problema está en uno de los inputs — encuentra y corrige el input, y el total se actualizará automáticamente.") },
    { g: t("System", "Sistema"), q: t("What is the Audit Trail and who can see it?", "¿Qué es el Audit Trail y quién puede verlo?"), a: t("The Audit Trail is the complete history of all manual changes. The Administrator (owner) has full access. Managers can see their own changes. Staff cannot access it. It is immutable — no one can delete or modify entries.", "El Audit Trail es el historial completo de todos los cambios manuales. El Administrador (dueño) tiene acceso completo. Los Managers pueden ver sus propios cambios. El staff no puede acceder. Es inmutable — nadie puede borrar ni modificar entradas.") },
    { g: t("System", "Sistema"), q: t("How often should I run the close process?", "¿Con qué frecuencia debo hacer el proceso de cierre?"), a: t("Every single operating night, without exception. The close of Day N becomes the opening of Day N+1. Skipping a close breaks the carry-forward chain and makes reconciliation impossible.", "Cada noche de operación, sin excepción. El cierre del Día N se convierte en la apertura del Día N+1. Saltarse un cierre rompe la cadena de carry-forward y hace imposible la reconciliación.") },
    { g: t("System", "Sistema"), q: t("How do I know if the system is working correctly?", "¿Cómo sé si el sistema está funcionando correctamente?"), a: t("Use the QA Validation Checklist in University → Checklists. If all items pass, the system is working correctly. Run this checklist before permanently switching from the Excel spreadsheet.", "Usa el Checklist de Validación QA en Universidad → Checklists. Si todos los ítems pasan, el sistema está funcionando correctamente. Ejecuta este checklist antes de cambiar permanentemente del Excel.") },
    { g: t("System", "Sistema"), q: t("Can the system connect directly to Toast to import data automatically?", "¿Puede el sistema conectarse directamente a Toast para importar datos automáticamente?"), a: t("Toast integration is planned for a future sprint. Currently, Toast data (Net Sales, Service Charge, Tips) is entered manually from the Toast End of Day report. The system is designed so that when the integration is live, the manual entry fields will be replaced by automatic imports.", "La integración con Toast está planeada para un sprint futuro. Actualmente, los datos de Toast (Net Sales, Service Charge, Propinas) se ingresan manualmente del reporte End of Day de Toast. El sistema está diseñado para que cuando la integración esté activa, los campos de entrada manual sean reemplazados por importaciones automáticas.") },
  ];

  return { MODULES, VIDEOS, CHECKLISTS, GLOSSARY, FAQ };
}

function ModuleModal({ mod, onClose, onComplete, isCompleted }) {
  const t = useT();
  return (
    <div className="fixed inset-0 z-[120] bg-black/80 backdrop-blur-sm flex items-start justify-center overflow-y-auto p-3 sm:p-6" onClick={onClose}>
      <div className="bg-ink border border-white/12 rounded-2xl w-full max-w-2xl my-4 shadow-2xl" onClick={(e) => e.stopPropagation()}>
        <div className="flex items-start justify-between gap-3 p-5 border-b border-white/10 sticky top-0 bg-ink rounded-t-2xl z-10">
          <div className="flex items-start gap-3">
            <div className="w-11 h-11 rounded-xl bg-coral/15 text-coral flex items-center justify-center shrink-0">
              <Icon name={mod.icon} className="w-5 h-5" />
            </div>
            <div>
              <div className="label text-white/40">{mod.cat}</div>
              <h3 className="text-lg font-semibold text-white leading-tight">{mod.title}</h3>
              <div className="flex items-center gap-2 mt-1.5">
                <Chip color="var(--c-coral)">{mod.level}</Chip>
                <span className="text-xs text-white/40">{mod.mins} min</span>
              </div>
            </div>
          </div>
          <button onClick={onClose} className="text-white/40 hover:text-white shrink-0"><Icon name="x" className="w-5 h-5" /></button>
        </div>
        <div className="p-5 space-y-5">
          {mod.steps.map((s, i) => (
            <div key={i} className="flex gap-3">
              <div className="w-7 h-7 rounded-full bg-coral/15 text-coral text-sm font-semibold flex items-center justify-center shrink-0">{i + 1}</div>
              <div className="flex-1">
                <h4 className="text-white font-semibold text-sm mb-1">{s.title}</h4>
                <p className="text-white/55 text-sm leading-relaxed">{s.body}</p>
                {s.rule && (
                  <div className="mt-2.5 flex gap-2 items-start bg-coral/10 border border-coral/25 rounded-xl px-3 py-2.5">
                    <Icon name="alert-triangle" className="w-4 h-4 text-coral shrink-0 mt-0.5" />
                    <p className="text-white/75 text-xs leading-relaxed">{s.rule}</p>
                  </div>
                )}
              </div>
            </div>
          ))}
        </div>
        <div className="p-5 border-t border-white/10 sticky bottom-0 bg-ink rounded-b-2xl">
          {isCompleted ? (
            <button onClick={() => onComplete(mod.id)} className="w-full py-3 rounded-xl bg-green-500/15 text-green-400 border border-green-500/30 font-semibold flex items-center justify-center gap-2">
              <Icon name="check-circle" className="w-5 h-5" />{t("Completed — tap to unmark", "Completado — toca para desmarcar")}
            </button>
          ) : (
            <button onClick={() => onComplete(mod.id)} className="w-full py-3 rounded-xl bg-coral hover:bg-coral-deep text-white font-semibold flex items-center justify-center gap-2 transition">
              <Icon name="check" className="w-5 h-5" />{t("Mark as completed", "Marcar completado")}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

function VideoModal({ vid, onClose }) {
  const t = useT();
  return (
    <div className="fixed inset-0 z-[120] bg-black/80 backdrop-blur-sm flex items-start justify-center overflow-y-auto p-3 sm:p-6" onClick={onClose}>
      <div className="bg-ink border border-white/12 rounded-2xl w-full max-w-2xl my-4 shadow-2xl" onClick={(e) => e.stopPropagation()}>
        <div className="flex items-center justify-between gap-3 p-5 border-b border-white/10">
          <div>
            <h3 className="text-lg font-semibold text-white leading-tight">{vid.title}</h3>
            <span className="text-xs text-white/40">{vid.mins} min</span>
          </div>
          <button onClick={onClose} className="text-white/40 hover:text-white shrink-0"><Icon name="x" className="w-5 h-5" /></button>
        </div>
        <div className="p-5">
          <div className="aspect-video rounded-xl bg-black/60 border border-white/10 flex flex-col items-center justify-center gap-3 mb-5">
            <div className="w-16 h-16 rounded-full bg-coral/20 text-coral flex items-center justify-center">
              <Icon name="play" className="w-7 h-7" />
            </div>
            <p className="text-white/40 text-sm">{t("Video coming soon", "Video disponible próximamente")}</p>
          </div>
          <div className="label text-white/40 mb-2">{t("Transcript", "Transcripción")}</div>
          <p className="text-white/55 text-sm leading-relaxed">{vid.transcript}</p>
        </div>
      </div>
    </div>
  );
}

function UniversityModule() {
  const t = useT();
  const { lang } = useLang();

  const [tab, setTab] = useState("guides");
  const [openMod, setOpenMod] = useState(null);
  const [openVid, setOpenVid] = useState(null);
  const [glossaryQ, setGlossaryQ] = useState("");
  const [openFaq, setOpenFaq] = useState(null);

  const [completed, setCompleted] = useState(() => {
    try { return (JSON.parse(localStorage.getItem("pp_univ") || "{}").completed) || []; } catch (e) { return []; }
  });
  const clKey = "pp_univ_cl_" + new Date().toISOString().slice(0, 10);
  const [clState, setClState] = useState(() => {
    try { return JSON.parse(localStorage.getItem(clKey) || "{}"); } catch (e) { return {}; }
  });

  const D = useMemo(() => univData(t), [lang]);
  const { MODULES, VIDEOS, CHECKLISTS, GLOSSARY, FAQ } = D;

  const persistCompleted = (next) => {
    setCompleted(next);
    try { localStorage.setItem("pp_univ", JSON.stringify({ completed: next })); } catch (e) {}
  };
  const toggleComplete = (id) => {
    const next = completed.includes(id) ? completed.filter((x) => x !== id) : [...completed, id];
    persistCompleted(next);
  };
  const persistCl = (next) => {
    setClState(next);
    try { localStorage.setItem(clKey, JSON.stringify(next)); } catch (e) {}
  };
  const toggleClItem = (clId, i) => {
    const cur = clState[clId] || [];
    const has = cur.includes(i);
    const nextCur = has ? cur.filter((x) => x !== i) : [...cur, i];
    persistCl({ ...clState, [clId]: nextCur });
  };

  const doneCount = completed.filter((id) => MODULES.some((m) => m.id === id)).length;
  const pct = Math.round((doneCount / MODULES.length) * 100);

  const TABS = [
    ["guides", t("Guides", "Guías"), "book-open"],
    ["videos", t("Videos", "Videos"), "play-circle"],
    ["checklists", t("Checklists", "Checklists"), "list-checks"],
    ["glossary", t("Glossary", "Glosario"), "book-a"],
    ["faq", "FAQ", "help-circle"],
  ];

  const glossarySorted = useMemo(() => {
    const q = glossaryQ.trim().toLowerCase();
    return GLOSSARY
      .filter((g) => !q || g.term.toLowerCase().includes(q) || g.def.toLowerCase().includes(q))
      .sort((a, b) => a.term.localeCompare(b.term));
  }, [GLOSSARY, glossaryQ]);

  const faqGroups = useMemo(() => {
    const map = {};
    FAQ.forEach((f, i) => { (map[f.g] = map[f.g] || []).push({ ...f, i }); });
    return Object.keys(map).map((g) => ({ g, items: map[g] }));
  }, [FAQ]);

  useReveal(tab);

  return (
    <div className="fade-view">
      <div className="flex items-start justify-between gap-4 flex-wrap mb-6">
        <div className="flex items-center gap-3">
          <div className="w-12 h-12 rounded-2xl bg-coral/15 text-coral flex items-center justify-center">
            <Icon name="graduation-cap" className="w-6 h-6" />
          </div>
          <div>
            <h2 className="text-2xl font-semibold text-white leading-tight">PinkPony University</h2>
            <p className="text-white/40 text-sm">{t("Learn to operate the system step by step", "Aprende a operar el sistema paso a paso")}</p>
          </div>
        </div>
        <div className="bg-card border border-white/10 rounded-2xl px-4 py-3 min-w-[180px]">
          <div className="flex items-center justify-between mb-1.5">
            <span className="label text-white/40">{t("Progress", "Progreso")}</span>
            <span className="text-sm font-semibold text-gold-soft">{doneCount}/{MODULES.length}</span>
          </div>
          <div className="h-2 rounded-full bg-white/10 overflow-hidden">
            <div className="h-full bg-coral rounded-full transition-all" style={{ width: pct + "%" }} />
          </div>
        </div>
      </div>

      <div className="flex gap-2 overflow-x-auto pb-2 mb-6 -mx-1 px-1">
        {TABS.map(([id, lbl, icon]) => (
          <button key={id} onClick={() => setTab(id)}
            className={"flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium whitespace-nowrap border transition " + (tab === id ? "bg-coral text-white border-coral" : "bg-card text-white/55 border-white/10 hover:text-white")}>
            <Icon name={icon} className="w-4 h-4" />{lbl}
          </button>
        ))}
      </div>

      {tab === "guides" && (
        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
          {MODULES.map((m) => {
            const done = completed.includes(m.id);
            return (
              <div key={m.id} className="bg-card border border-white/10 rounded-2xl p-5 flex flex-col">
                <div className="flex items-start justify-between mb-3">
                  <div className="w-11 h-11 rounded-xl bg-coral/15 text-coral flex items-center justify-center">
                    <Icon name={m.icon} className="w-5 h-5" />
                  </div>
                  {done && <span className="text-green-400"><Icon name="check-circle" className="w-5 h-5" /></span>}
                </div>
                <div className="label text-white/35 mb-1">{m.cat}</div>
                <h3 className="text-white font-semibold leading-tight mb-1.5">{m.title}</h3>
                <p className="text-white/55 text-sm leading-relaxed flex-1">{m.desc}</p>
                <div className="flex items-center gap-2 mt-3 mb-4">
                  <Chip color="var(--c-coral)">{m.level}</Chip>
                  <span className="text-xs text-white/40">{m.mins} min</span>
                </div>
                <button onClick={() => setOpenMod(m)}
                  className="w-full py-2.5 rounded-xl bg-coral hover:bg-coral-deep text-white text-sm font-semibold flex items-center justify-center gap-2 transition">
                  <Icon name={done ? "rotate-ccw" : "play"} className="w-4 h-4" />{done ? t("Review", "Repasar") : t("Start", "Empezar")}
                </button>
              </div>
            );
          })}
        </div>
      )}

      {tab === "videos" && (
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
          {VIDEOS.map((v) => (
            <div key={v.id} className="bg-card border border-white/10 rounded-2xl overflow-hidden flex flex-col">
              <button onClick={() => setOpenVid(v)} className="relative aspect-video flex items-center justify-center"
                style={{ background: "linear-gradient(135deg, rgba(234,93,129,.35), rgba(120,60,180,.35))" }}>
                <div className="w-14 h-14 rounded-full bg-black/40 backdrop-blur flex items-center justify-center text-white">
                  <Icon name="play" className="w-6 h-6" />
                </div>
                <span className="absolute bottom-2 right-2 text-xs bg-black/60 text-white px-2 py-0.5 rounded-md">{v.mins} min</span>
              </button>
              <div className="p-4 flex flex-col flex-1">
                <h3 className="text-white font-semibold leading-tight mb-1">{v.title}</h3>
                <p className="text-white/55 text-sm leading-relaxed flex-1">{v.desc}</p>
                <button onClick={() => setOpenVid(v)} className="mt-3 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-white text-sm font-semibold flex items-center justify-center gap-2 transition border border-white/10">
                  <Icon name="play" className="w-4 h-4" />{t("Watch", "Ver")}
                </button>
              </div>
            </div>
          ))}
        </div>
      )}

      {tab === "checklists" && (
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
          {CHECKLISTS.map((cl) => {
            const checked = clState[cl.id] || [];
            const allDone = checked.length === cl.items.length;
            return (
              <div key={cl.id} className="bg-card border border-white/10 rounded-2xl p-5">
                <div className="flex items-center gap-3 mb-4">
                  <div className="w-10 h-10 rounded-xl bg-coral/15 text-coral flex items-center justify-center">
                    <Icon name={cl.icon} className="w-5 h-5" />
                  </div>
                  <div className="flex-1">
                    <h3 className="text-white font-semibold leading-tight">{cl.title}</h3>
                    <div className="label text-white/35">{cl.role}</div>
                  </div>
                  <span className="text-sm font-semibold text-gold-soft">{checked.length}/{cl.items.length}</span>
                </div>
                {allDone && (
                  <div className="fade-view is-in mb-3 flex items-center gap-2 bg-green-500/15 border border-green-500/30 rounded-xl px-3 py-2.5">
                    <Icon name="check-circle" className="w-5 h-5 text-green-400" />
                    <span className="text-green-400 text-sm font-semibold">{t("All done for today", "Todo listo por hoy")}</span>
                  </div>
                )}
                <div className="space-y-1.5">
                  {cl.items.map((item, i) => {
                    const on = checked.includes(i);
                    return (
                      <button key={i} onClick={() => toggleClItem(cl.id, i)}
                        className="w-full flex items-start gap-3 text-left px-3 py-2.5 rounded-xl hover:bg-white/5 transition">
                        <span className={"w-5 h-5 rounded-md border flex items-center justify-center shrink-0 mt-0.5 " + (on ? "bg-coral border-coral text-white" : "border-white/25")}>
                          {on && <Icon name="check" className="w-3.5 h-3.5" />}
                        </span>
                        <span className={"text-sm leading-relaxed " + (on ? "text-white/40 line-through" : "text-white/75")}>{item}</span>
                      </button>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      )}

      {tab === "glossary" && (
        <div>
          <div className="relative mb-5 max-w-md">
            <span className="absolute left-3 top-1/2 -translate-y-1/2 text-white/35"><Icon name="search" className="w-4 h-4" /></span>
            <input value={glossaryQ} onChange={(e) => setGlossaryQ(e.target.value)}
              placeholder={t("Search terms...", "Buscar términos...")}
              className="w-full bg-card border border-white/10 rounded-xl pl-10 pr-3 py-2.5 text-sm text-white placeholder-white/35 outline-none focus:border-coral" />
          </div>
          {glossarySorted.length === 0 ? (
            <p className="text-white/40 text-sm">{t("No terms match your search.", "Ningún término coincide con tu búsqueda.")}</p>
          ) : (
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
              {glossarySorted.map((g, i) => (
                <div key={i} className="bg-card border border-white/10 rounded-2xl p-4">
                  <h4 className="text-gold-soft font-semibold text-sm mb-1">{g.term}</h4>
                  <p className="text-white/55 text-sm leading-relaxed">{g.def}</p>
                </div>
              ))}
            </div>
          )}
        </div>
      )}

      {tab === "faq" && (
        <div className="space-y-6">
          {faqGroups.map((grp) => (
            <div key={grp.g}>
              <div className="label text-white/40 mb-2">{grp.g}</div>
              <div className="space-y-2">
                {grp.items.map((f) => {
                  const on = openFaq === f.i;
                  return (
                    <div key={f.i} className="bg-card border border-white/10 rounded-2xl overflow-hidden">
                      <button onClick={() => setOpenFaq(on ? null : f.i)} className="w-full flex items-center justify-between gap-3 px-4 py-3.5 text-left">
                        <span className="text-white text-sm font-medium">{f.q}</span>
                        <Icon name="chevron-down" className={"w-4 h-4 text-white/40 shrink-0 transition-transform " + (on ? "rotate-180" : "")} />
                      </button>
                      {on && <div className="px-4 pb-4 -mt-1"><p className="text-white/55 text-sm leading-relaxed">{f.a}</p></div>}
                    </div>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
      )}

      {openMod && <ModuleModal mod={openMod} onClose={() => setOpenMod(null)} onComplete={toggleComplete} isCompleted={completed.includes(openMod.id)} />}
      {openVid && <VideoModal vid={openVid} onClose={() => setOpenVid(null)} />}
    </div>
  );
}

Object.assign(window, { UniversityModule });
