/* MonkeyCams — Inventario: KPIs, tabla, tarjetas, controles, filter sheet */

/* ============================================
   FRANJA DE RESUMEN
   Sustituye a las siete tarjetas KPI con barra de porcentaje: sobre 26
   cámaras, la mitad marcaban 0 permanentemente y el porcentaje no decía
   nada que no dijera ya el número. Ver docs/PRODUCT.md (anti-referencias:
   «dashboards con KPIs») y el comentario de renderStats en inventory.js.
============================================ */
.stats-dashboard {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 8px;
    margin: 0 auto 16px;
    /* Misma columna de contenido que gráfico/toolbar/listas. */
    max-width: var(--content-max-table);
}

/* Chip: **el mismo en móvil y en escritorio**, a petición del usuario
   (2026-07-29, tercera vuelta). Antes el fondo solo se ponía por debajo de
   768px y en escritorio quedaba una línea de texto con puntos «·» de
   separador: dos diseños distintos para el mismo control, y el de móvil
   gustaba más. Ahora el chip es el estilo base y no hay variante. */
.statline-item {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 5px 10px;
    background: var(--bg-surface);
    border: 0;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.8125rem;
    line-height: 1.3;
    cursor: pointer;
    transition: background 0.15s var(--ease-out), color 0.15s var(--ease-out);
}

.statline-num {
    font-family: "DM Mono", monospace;
    font-variant-numeric: tabular-nums;
    font-size: 1.0625rem;
    font-weight: 500;
    line-height: 1;
    color: var(--dot-color, var(--accent));
}

.statline-label {
    letter-spacing: -0.01em;
}

@media (hover: hover) {
    .statline-item:hover {
        background: var(--bg-surface);
        color: var(--text-main);
    }
}

.statline-item:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.statline-item:active {
    transform: scale(0.97);
}

.statline-item.is-active {
    background: color-mix(in srgb, var(--dot-color, var(--accent)) 14%, transparent);
    color: var(--text-main);
}

/* En táctil, cada número es un objetivo de pulsación: se sube a los 44px
   recomendados sin engordar la franja en escritorio. */
@media (pointer: coarse) {
    .statline-item {
        min-height: 44px;
        align-items: center;
        padding: 6px 10px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .statline-item {
        transition: none;
    }
    .statline-item:active {
        transform: none;
    }
}

/* En móvil, rejilla de 3 columnas: seis entradas caen en 3 + 3, dos filas
   parejas. El historial de esto, porque las tres vueltas fueron el mismo día
   y cada una descartó la anterior por un motivo distinto:
     1. Línea de texto con puntos «·». Al no caber envolvía dejando un punto
        huérfano y solo el ítem activo con fondo → las filas de abajo se leían
        como texto suelto.
     2. Chips con `flex-wrap`. Mejor, pero con seis anchos distintos salían
        tres filas desiguales (2 + 3 + 1) y un hueco grande al final.
     3. Tira de scroll horizontal en una fila. Cabía, pero escondía la mitad
        de los números detrás de un gesto.
   La rejilla las pone todas a la vista en dos filas simétricas, sin depender
   de cuánto mida cada etiqueta. Con menos de seis entradas (los estados sin
   cámaras no se pintan, ver `summaryEntries`) el reparto sigue siendo de tres
   por fila, que es lo predecible.

   `minmax(0, 1fr)` y no `1fr`: si alguna etiqueta creciera hasta pasar del
   tercio de pantalla, con `1fr` el mínimo automático de la columna sería ese
   contenido y la rejilla se desbordaría a lo ancho; con `minmax(0, …)` la
   etiqueta pasa a dos líneas y las tres columnas siguen siendo iguales. */
@media (max-width: 768px) {
    .stats-dashboard {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .statline-item {
        /* La rejilla ya da el ancho; el chip lo ocupa entero para que las
           dos filas queden alineadas en columnas. */
        justify-content: center;
        text-align: center;
    }
}

/* ============================================
           BRAND CHART
        ============================================ */
.chart-section {
    background: var(--bg-surface);
    border-radius: var(--radius-sm);
    padding: 16px 18px;
    margin-bottom: 14px;
}

.chart-section-title {
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-muted);
    margin-bottom: 14px;
}

.chart-bars {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.chart-row {
    display: grid;
    grid-template-columns: 80px 1fr 32px;
    align-items: center;
    gap: 10px;
}

.chart-brand-label {
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--text-muted);
    text-align: right;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chart-track {
    height: 8px;
    background: var(--bg-hover);
    border-radius: 4px;
    overflow: hidden;
    display: flex;
}

.chart-segment {
    height: 100%;
    animation: chartReveal 0.5s var(--ease-out) both;
}
@keyframes chartReveal {
    from { opacity: 0; }
    to   { opacity: 1; }
}
/* Al volver a una pestaña ya vista (data-revealed la pone view-tabs.js al
   ocultarla), display:none→block reiniciaría la animación: no repetirla. */
[data-revealed] .chart-segment {
    animation: none;
}
@media (prefers-reduced-motion: reduce) {
    .chart-segment {
        animation: none;
    }
}

.chart-segment:first-child {
    border-radius: 4px 0 0 4px;
}
.chart-segment:last-child {
    border-radius: 0 4px 4px 0;
}
.chart-segment:only-child {
    border-radius: 4px;
}

.chart-count {
    font-family: "DM Mono", monospace;
    font-size: 0.72rem;
    color: var(--text-faint);
    text-align: right;
}

/* ============================================
           BARRA DE CONTROLES
        ============================================ */
.controls-bar {
    background: var(--bg-surface);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    margin-bottom: 14px;
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    align-items: center;
}


.btn {
    cursor: pointer;
    font-weight: 500;
    transition:
        border-color 0.15s var(--ease-out),
        box-shadow 0.15s var(--ease-out),
        background 0.15s var(--ease-out),
        transform 0.08s var(--ease-out);
}

@media (hover: hover) {
    /* Excluir familias con hover propio (sólidos/tinte vía ac-btn, pills, iconos, barras). */
    .btn:hover:not(:is(
        .ac-btn,
        .ac-icon-btn,
        .ac-segment-btn,
        .icon-btn,
        .btn-accent,
        .btn-muted,
        .btn-danger
    )) {
        border-color: var(--accent);
        background: var(--accent-dim);
        color: var(--accent);
    }
}

.btn:active {
    transform: scale(0.97);
}

.btn.active {
    background: var(--accent-fill, var(--accent));
    border-color: var(--accent);
    color: #fff;
}


.icon-btn {
    padding: 6px 7px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.icon-btn svg {
    width: var(--ac-icon-md);
    height: var(--ac-icon-md);
}

/* Puntero táctil: más área de pulsación en los botones del encabezado.
   14px de padding + 15px de icono + borde ≈ 45px de objetivo (mínimo
   recomendado 44px; con 11px se quedaba en ~37px). */
@media (pointer: coarse) {
    .header-right .icon-btn {
        padding: 14px;
        min-width: 44px;
        min-height: 44px;
    }
    .controls-bar .icon-btn {
        padding: 14px;
        min-width: 44px;
        min-height: 44px;
    }
    /* Los mismos botones, pero en la barra de los CATÁLOGOS (Cámaras y
       Películas), que no es `.controls-bar` y por eso se quedaba fuera de la
       regla de arriba: medían 30×28 en móvil — la mitad del mínimo — siendo
       el interruptor de vista, que se toca constantemente. Encontrado el
       2026-08-18 auditando Películas, y resultó estar igual en Cámaras. */
    .catalog-view-toolbar .btn-group-views .icon-btn {
        padding: 14px;
        min-width: 44px;
        min-height: 44px;
    }
}


.btn-group-views {
    display: flex;
    gap: 4px;
    padding-right: 0;
    border-right: none;
}

.btn-group-theme {
    display: flex;
    gap: 4px;
}

.controls-filters {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    align-items: center;
}

.filter-reset-btn {
    flex-shrink: 0;
}

.table-view-toolbar {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    max-width: var(--content-max-table);
    margin: 0 auto 8px;
    padding: 0 2px;
    min-height: 28px;
}

.table-view-toolbar[hidden] {
    display: none;
}

.table-group-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.78rem;
    color: var(--text-muted);
    cursor: pointer;
    user-select: none;
}

.table-group-toggle input[type="checkbox"] {
    width: 13px;
    height: 13px;
    padding: 0;
    cursor: pointer;
    accent-color: var(--accent);
}

.controls-right {
    display: flex;
    gap: 6px;
    align-items: center;
    margin-left: auto;
    flex-shrink: 0;
}

/* ============================================
           TABLA
        ============================================ */
.table-wrapper {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-surface);
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
    white-space: nowrap;
}

thead {
    position: sticky;
    top: 0;
    z-index: 10;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
}

th {
    text-align: left;
    padding: 10px 14px;
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--text-faint);
    cursor: pointer;
    user-select: none;
    transition: color 0.15s;
}

th.active {
    color: var(--accent);
}

@media (hover: hover) {
    th:hover {
        color: var(--accent);
    }
}
th .sort-icon {
    margin-left: 3px;
    opacity: 0.35;
    font-size: 0.6rem;
}
th.active .sort-icon {
    opacity: 1;
}
th:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

td {
    padding: 9px 14px;
    border-bottom: 1px solid var(--border-subtle);
    color: var(--text-main);
    font-size: 0.875rem;
}

tbody tr:last-child td {
    border-bottom: none;
}

@media (hover: hover) {
    tbody tr:hover {
        background: var(--bg-hover);
    }
}

tbody tr.urgent {
    background: color-mix(in srgb, var(--err-color) 5%, transparent);
}
tbody tr.has-film {
    background: color-mix(in srgb, var(--loaded-color) 5%, transparent);
}

.col-brand {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-faint);
}

/* El único texto de la tabla que puede ir en dos líneas.
   `table { white-space: nowrap }` vale para todo lo demás —insignias, seriales,
   marcas— pero con las digitales dentro (2026-08-22) el modelo más largo pasó
   de «AE-1 Program» a «OM-D E-M10 Mark IV», 45px más, y la tabla dejaba de
   caber a 1440px por ese solo nombre. Recortarlo con puntos suspensivos no es
   una opción: el modelo ES la cámara.
   126px es el mismo ancho que le da el <colgroup> de la vista agrupada, así
   que las dos tablas parten el nombre por el mismo sitio. Un modelo que quepa
   —todos los demás— sigue en una línea: `max-width` no fuerza el salto, solo
   lo permite cuando hace falta. */
.col-model {
    font-weight: 500;
    white-space: normal;
    max-width: 126px;
}

.col-serial {
    font-family: "DM Mono", monospace;
    font-size: 0.78rem;
    color: var(--text-faint);
}

/* Celda que no aplica: las tres de carrete (velocidades, 35 mm, cargada) en
   una cámara digital. La raya va apagada y centrada para que se lea como un
   hueco a propósito y no como un dato que falta — que es la diferencia entre
   «esta cámara no tiene carrete» y «no lo he rellenado». */
.col-na {
    color: var(--text-faint);
    text-align: center;
}

.col-notes {
    /* Mismo tamaño que los badges de estado/velocidades de la fila: el
       texto de observaciones destacaba por ser visiblemente más grande
       que los badges y que los chips de etiqueta (.ac-pill) de la misma celda. */
    font-size: 0.7rem;
    color: var(--text-muted);
    white-space: normal;
    min-width: 180px;
    max-width: 380px;
    line-height: 1.4;
    text-wrap: pretty;
    /* Arriba, igual que Etiquetas: son las dos columnas que crecen en alto, y
       con el `middle` por defecto una observación quedaba flotando en mitad de
       la fila mientras los chips de al lado arrancaban pegados al borde. Las
       columnas de insignia se quedan centradas: son de una sola línea y ahí
       centrar es lo correcto. */
    vertical-align: top;
}
.col-notes .ac-pill {
    font-size: 0.7rem;
}

/* Etiquetas, en su propia columna desde el 2026-07-30 (antes compartía celda
   con Observaciones). `vertical-align: top` porque los chips arrancan arriba y
   una observación de tres líneas al lado dejaría los de esta celda centrados
   contra el texto de enfrente. */
.col-tags {
    min-width: 150px;
    max-width: 240px;
    vertical-align: top;
    /* 10px de lado y no los 16 del resto de celdas: los chips ya traen su
       propio relleno, así que el de la celda solo separa del borde. Los 12px
       que se ahorran son los que hacen que la tabla entera quepa a 1440px en
       vez de pasarse por un píxel — y un píxel de más saca una barra de scroll
       igual que cien. */
    padding-left: 10px;
    padding-right: 10px;
}
.col-tags .cam-tags {
    /* Sin margen superior: ahí lo tenía para despegarse del texto de
       observaciones que llevaba encima en la misma celda, y ya no hay nada. */
    margin-top: 0;
}

/* ============================================
           ETIQUETAS EN TARJETA (vista compacta por marca)
        ============================================ */
.view-table {
    display: block;
}

/* Micro-título de sección para que los chips de etiqueta no se lean como
   observaciones — usado por wrapTagsBlock() en inventory.js */
.card-tags-block {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--border-subtle);
}
.card-tags-label {
    font-size: 0.62rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 5px;
}
.card-tags-block .cam-tags {
    margin-top: 0;
}

/* ============================================
           GRUPOS POR MARCA
        ============================================ */
.grouped-table-wrapper {
    /* Las seis primeras columnas suman 789px fijos y Etiquetas otros 178; por
       debajo de este ancho, Observaciones bajaría de las ~180px en las que su
       texto deja de leerse.
       Subió de 980 a 1147 al separar Etiquetas de Observaciones (2026-07-30), y
       eso significa que a 1280px esta vista pasa a deslizarse en horizontal
       cuando antes cabía justa. Es el precio de una séptima columna y no hay
       de dónde sacarlo: recortando las tres columnas de insignia a su contenido
       exacto —sin el aire que las protege de un valor más largo— se ganan 69px
       y harían falta 145. Deslizar es además el mecanismo que esta vista ya
       tiene previsto: scrollea el contenedor entero, así que las columnas
       siguen alineadas entre marcas mientras se desplaza. */
    --grouped-table-min: 1147px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    max-width: var(--content-max-table);
    margin: 0 auto;
    background: transparent;
    /* Las tarjetas no bajan de --grouped-table-min (ver .brand-group), así
       que por debajo de ese ancho scrollea el CONTENEDOR y no cada tarjeta:
       todas se desplazan a la vez y las columnas siguen alineadas. */
    overflow-x: auto;
    overflow-y: visible;
}

.brand-group {
    background: var(--bg-surface);
    border-radius: var(--radius-lg);
    overflow: hidden;
    min-width: var(--grouped-table-min);
}

/* Las columnas las fija el <colgroup> que monta renderGroupedTable(), y son
   las MISMAS para todas las marcas. Sin esto cada tabla se dimensionaba por
   su propio contenido y las columnas bailaban de un grupo al siguiente. */
.brand-group-content table {
    table-layout: fixed;
}

/* Red de seguridad: con anchos fijos, un texto más largo de lo previsto se
   derramaría sobre la columna vecina en vez de ensancharla. Observaciones y
   Etiquetas quedan fuera — esas sí van en varias líneas (la primera porque es
   texto largo, la segunda porque sus chips envuelven). */
.brand-group-content td:not(.col-notes):not(.col-tags):not(.col-model),
.brand-group-content th {
    overflow: hidden;
    text-overflow: ellipsis;
}

.brand-group-header {
    background: var(--bg-raised);
    border: none;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: 10px 14px;
    font-weight: 600;
    font-size: 0.9rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    cursor: pointer;
    user-select: none;
    transition: background 0.15s;
}

.brand-group.collapsed .brand-group-header {
    border-radius: var(--radius-lg);
}

@media (hover: hover) {
    .brand-group-header:hover {
        background: var(--bg-hover);
    }
}
.brand-group-header:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.brand-group-count {
    font-family: "DM Mono", monospace;
    font-size: 0.7rem;
    color: var(--text-faint);
    font-weight: 400;
}

.brand-group-content {
    border: none;
    border-radius: 0;
    overflow: hidden;
}

.brand-group.collapsed .brand-group-content {
    display: none;
}
.brand-group-content thead {
    background: var(--bg-raised);
}

/* ============================================
           EMPTY STATE
        ============================================ */
.empty-state {
    text-align: center;
    padding: 52px 24px;
    color: var(--text-faint);
    font-size: 0.9rem;
}

/* ============================================
           RESPONSIVE
        ============================================ */
@media (max-width: 768px) {
    /* Patrón Mail/Files: buscador + botón de filtro en una sola fila (el
       botón ya no vive solo, apilado debajo con un hueco al lado — ver
       .filter-sheet-btn más abajo, ahora un icono circular junto al
       buscador en vez de un pill de texto separado). .controls-right ya
       no aporta nada en móvil (su único contenido, reset ⟳ y el switcher
       de vistas, está oculto más abajo). */
    .controls-bar {
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 10px;
    }
    .search-box {
        flex: 1;
        min-width: 0;
        min-height: var(--ac-filter-h, 44px);
    }
    .controls-right {
        display: none;
    }
}

/* ============================================
           ANIMACIONES
        ============================================ */
@media (prefers-reduced-motion: no-preference) {
    tbody tr {
        animation: rowIn 0.1s ease-out both;
    }
    .chart-segment {
        animation: barIn 0.4s ease-out;
        transform-origin: left center;
    }

    @keyframes rowIn {
        from {
            opacity: 0;
            transform: translateY(-2px);
        }
        to {
            opacity: 1;
            transform: none;
        }
    }
    @keyframes barIn {
        from {
            transform: scaleX(0);
        }
        to {
            transform: scaleX(1);
        }
    }
}

/* ============================================
   ETIQUETAS (tags) — visualización en tabla y tarjetas
============================================ */
.cam-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 5px;
}

/* ============================================
   FICHA LATERAL (drawer) de cámara — solo lectura
============================================ */
.cam-scrim {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
    z-index: var(--z-scrim);
}
.cam-scrim.on {
    opacity: 1;
    pointer-events: auto;
}
.cam-drawer {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    height: 100dvh;
    width: 420px;
    max-width: 92vw;
    background: var(--bg-surface);
    border-left: 1px solid var(--border);
    box-shadow: -1px 0 0 var(--border);
    transform: translateX(100%);
    transition: transform 0.26s var(--ease-out);
    z-index: var(--z-drawer);
    display: flex;
    flex-direction: column;
}
.cam-drawer.on {
    transform: none;
}
.cam-drawer-head {
    position: relative;
    padding: 22px 56px 22px 24px;
    border-bottom: 1px solid var(--border-subtle);
}
.cam-drawer-close {
    position: absolute;
    top: 18px;
    right: 18px;
}
.cam-drawer-brand {
    font-size: 0.74rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.cam-drawer-model {
    font-size: 1.6rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    margin: 2px 0 12px;
}
.cam-drawer-meta {
    display: flex;
    align-items: center;
    gap: 10px;
}
.cam-drawer-sn {
    font-family: "DM Mono", monospace;
    font-size: 0.78rem;
    color: var(--text-faint);
}
.cam-drawer-body {
    padding: 20px 24px;
    overflow-y: auto;
    flex: 1;
    display: flex;
    flex-direction: column;
}
/* El flex column de arriba está solo para que .cam-drawer-actions pueda
   empujarse al fondo con margin-top:auto. El efecto secundario es que TODO
   hijo pasa a ser un flex item con flex-shrink:1, así que cuando la ficha no
   cabe, el navegador la aprieta en vez de dejar que el panel haga scroll.
   Quien más lo pagaba era la foto: es lo único con `aspect-ratio` y sin
   altura mínima intrínseca, así que absorbía casi todo el aplastamiento —
   medido en un 390x844 con carretes y partes, 310x232 se quedaba en 310x61,
   una franja que no parece una foto. En escritorio no se veía porque la
   ficha cabía entera y no había nada que apretar. */
.cam-drawer-body > * {
    flex-shrink: 0;
}
.cam-fgrid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}
.cam-field {
    background: var(--bg-raised);
    border: 1px solid var(--border-subtle);
    border-radius: 11px;
    padding: 12px 14px;
}
.cam-full {
    grid-column: 1 / -1;
}
/* Con "Año de lanzamiento" presente hay 5 campos + Observaciones (6 hijos):
   la rejilla de 2 columnas dejaba el 5º colgando con un hueco al lado. El
   primero (Año) pasa a fila completa cuando el total es par — como fila de
   metadatos de cabecera — y los 4 restantes forman un 2×2 exacto. Sin año
   (5 hijos, total impar) la regla no aplica y el 2×2 ya sale solo. */
.cam-fgrid .cam-field:first-child:nth-last-child(even) {
    grid-column: 1 / -1;
}
.cam-k {
    font-size: 0.64rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
    font-weight: 600;
    margin-bottom: 6px;
}
.cam-v {
    font-size: 0.98rem;
    font-weight: 600;
}
.cam-v.cam-notes {
    font-weight: 400;
    font-style: italic;
    color: var(--text-muted);
    line-height: 1.55;
    text-wrap: pretty;
}
.cam-sectit {
    font-size: 0.64rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-faint);
    font-weight: 600;
    margin: 20px 0 8px;
}
.cam-tags {
    display: flex;
    gap: 7px;
    flex-wrap: wrap;
}
/* "Sin etiquetas" en la ficha lateral: único span sin clase .ac-pill que
   queda en un contenedor .cam-tags (no es una etiqueta real, no debe verse
   como un pill acento). */
.cam-drawer-body .cam-tags .cam-empty {
    color: var(--text-faint);
    opacity: 0.6;
}
.cam-hint {
    margin-top: 20px;
    font-size: 0.78rem;
    color: var(--text-faint);
}
/* Indicar que las filas/tarjetas son pulsables (modo vista) */
body:not(.edit-mode) tbody tr[data-serial] { cursor: pointer; }

/* ============================================
   Pulido de cabecera (look más limpio/plano)
============================================ */
.app-header {
    padding-bottom: 16px;
    margin-bottom: 16px;
}

/* ============================================
   MÓVIL — inventario sin scroll lateral
   (Tabla y Tablero implican scroll horizontal: se ocultan;
    en su lugar se usa la vista de Tarjetas, apilada en vertical.)
============================================ */
/* Botón «Filtrar» solo en móvil (ver @media max-width 768px más abajo).
   Solo icono, junto al buscador en la misma fila (patrón Mail/Files: campo
   de búsqueda + botón de filtro adyacente) — antes era un pill con texto
   que vivía solo, apilado debajo del buscador. El conteo de filtros activos
   va en una insignia numérica sobre el icono, no en el texto del botón. */
.filter-sheet-btn {
    display: none;
    position: relative;
    align-items: center;
    justify-content: center;
    width: var(--ac-filter-h, 44px);
    height: var(--ac-filter-h, 44px);
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--bg-raised);
    color: var(--text-main);
    flex: 0 0 auto;
}
.filter-sheet-btn svg {
    width: var(--ac-icon-lg);
    height: var(--ac-icon-lg);
    flex: 0 0 auto;
}
/* El botón de la guía de modos usa este mismo componente, pero existe también
   en escritorio: el de filtros no, porque allí los filtros están a la vista y
   no hay capa que abrir. La regla base es `display: none` y se enciende en el
   media query de móvil, así que esto lo enciende en los dos. */
.filter-sheet-btn--always {
    display: inline-flex;
}

/* Con filtros activos: icono tintado accent (estilo iOS), no borde */
.filter-sheet-btn--active {
    background: var(--accent-dim);
    color: var(--accent);
}
.filter-sheet-btn-count {
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: var(--radius-pill);
    background: var(--accent-fill, var(--accent));
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    box-shadow: 0 0 0 2px var(--bg-body);
}

@media (max-width: 768px) {
    #tableViewBtn {
        display: none;
    }
    /* En móvil los filtros van al sheet; la barra solo muestra búsqueda + botón */
    .controls-filters {
        display: none;
    }
    .filter-sheet-btn {
        display: inline-flex;
    }
    .filter-active-banner {
        margin-bottom: 10px;
    }
}
tbody tr:focus-visible > td {
    outline: none;
    background: rgba(var(--accent-rgb), 0.06);
}
tbody tr:focus-visible > td:first-child {
    box-shadow: inset 3px 0 0 var(--accent);
}
/* ── Gráfica por marca dentro de tarjeta (compacta) ── */
.chart-section {
    background: var(--bg-surface);
    border-radius: var(--radius-lg);
    padding: 16px 20px;
    margin: 0 auto 14px;
    border: none;
    max-width: var(--content-max-table);
}

/* ── Controles flotantes — sin fondo de contenedor, sin bordes ── */
.controls-bar {
    max-width: var(--content-max-table);
    margin: 0 auto 12px;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    border: none;
    background: transparent;   /* flota directo sobre el fondo negro */
}
/* Search: layout en barra de controles (apariencia en .ac-search-input). */
.search-box {
    flex: 1;
    min-width: 220px;
}
/* Selects tipo píldora: sizing en components.css (--ac-filter-h) */
.select-pill:focus {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-dim);
}

/* Banner de filtros activos (inventario) */
.filter-active-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    max-width: var(--content-max-table);
    margin: 0 auto 12px;
    padding: 10px 14px;
    border-radius: var(--radius-md);
    background: var(--bg-surface);
    color: var(--text-muted);
    font-size: 0.82rem;
    line-height: 1.45;
}
.filter-active-banner[hidden] {
    display: none;
}
.filter-active-banner.is-empty {
    background: rgba(var(--accent-rgb), 0.1);
    box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.28);
    color: var(--text-main);
}
.filter-active-banner-text {
    flex: 1;
    min-width: 0;
}
.filter-active-banner-clear {
    flex-shrink: 0;
    padding: 6px 14px;
    border: none;
    border-radius: var(--radius-pill);
    background: var(--accent-dim);
    color: var(--accent);
    font: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    cursor: pointer;
}
@media (hover: hover) {
    .filter-active-banner-clear:hover {
        background: rgba(var(--accent-rgb), 0.22);
    }
}
.btn.icon-btn {
    border-radius: 50%;
    background: var(--bg-raised);
    border: none;
}
/* Estado activo (p. ej. modo edición ON) — este bloque debe ir después de
   .btn.icon-btn: misma cascada que pisaba el .btn.active genérico de arriba. */
.btn.icon-btn.active {
    background: var(--accent-fill, var(--accent));
    color: #fff;
}
.btn-group-views {
    background: var(--bg-raised);
    border-radius: var(--radius-pill);
    padding: 3px;
    border: none;
}
.btn-group-views .btn.icon-btn {
    border: none;
    background: transparent;
    border-radius: var(--radius-pill);
}
.btn-group-views .btn.icon-btn.active {
    background: var(--accent-fill, var(--accent));
    color: #fff;
}

/* ── Tabla sin borde exterior — mismo tratamiento que recipe-card / news-card ── */
.table-wrapper {
    background: var(--bg-surface);
    border-radius: var(--radius-lg);
    border: none;
    overflow-x: auto;
    overflow-y: hidden;
    max-width: var(--content-max-table);
    margin: 0 auto;
}
thead th {
    background: transparent;
    border-bottom: 1px solid var(--border);
    color: var(--text-muted);
    font-weight: 500;
    font-size: 0.8rem;
    text-transform: none;
    letter-spacing: -0.01em;
    /* 12px y no 16px en horizontal: son las CABECERAS las que fijan el ancho
       mínimo de la tabla (su texto es más largo que el de los datos, y todo
       va en nowrap). Con 16px la tabla pedía 1012px contra los 1002px de
       columna de contenido que hay a 1280px de ventana, así que aparecía una
       barra de scroll horizontal por 10px. Las celdas de datos mantienen los
       16px, que es lo que marca el ritmo visual de la tabla. */
    padding: 9px 12px;
}
tbody td {
    border-bottom: 1px solid var(--border-subtle);
    padding: 10px 16px;
}
tbody tr:last-child td { border-bottom: none; }
/* ── Dashboard colapsable ── */
.dashboard-section {
    max-width: var(--content-max-table); /* misma columna que KPIs/gráfico/toolbar/listas */
    margin: 0 auto 14px;
}
.dashboard-toggle {
    display: none;
    width: 100%;
    justify-content: space-between;
    align-items: center;
    /* 12px y no 10 arriba/abajo: solo existe en móvil (`display: none` aquí,
       se enciende más abajo) y con 10 se quedaba en 41px de alto, tres por
       debajo del mínimo táctil de 44 de esta app. */
    padding: 12px 14px;
    border: none;
    border-radius: var(--radius-md);
    background: var(--bg-surface);
    color: var(--text-main);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    appearance: none;
    -webkit-appearance: none;
    overflow: hidden;
    transition: background 0.15s var(--ease-out), transform 0.12s var(--ease-out);
}
.dashboard-toggle:focus {
    outline: none;
}
.dashboard-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
@media (hover: hover) {
    .dashboard-toggle:hover {
        background: rgba(var(--ac-overlay-white), 0.06);
    }
}
.dashboard-toggle:active {
    background: rgba(var(--accent-rgb), 0.14);
    transform: scale(0.99);
}
.dashboard-toggle[aria-expanded="true"] {
    box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.28);
}
.dashboard-toggle-hint {
    font-weight: 400;
    font-size: 0.78rem;
    color: var(--text-muted);
}
/* El plegable de la guía de modos (2026-08-22) usa este mismo botón, pero
   existe TAMBIÉN en escritorio. El de Marcas se esconde ahí a propósito —el
   gráfico cabe y no estorba—, mientras que la guía son dieciocho cámaras y 58
   modos: sin manera de plegarla en escritorio, el inventario quedaría debajo
   de varias pantallas de texto. Va después de la regla base para ganarle el
   `display: none` a igual especificidad. */
.dashboard-scope-note {
    margin: 0 0 8px;
    font-size: 0.75rem;
    color: var(--text-muted);
    text-align: center;
}
@media (max-width: 768px) {
    .dashboard-toggle {
        display: flex;
        margin-bottom: 10px;
    }
    .dashboard-section {
        margin-bottom: 22px;
    }
    .dashboard-body {
        margin-bottom: 8px;
    }
    .dashboard-section.is-collapsed .dashboard-body {
        display: none;
    }
    .controls-bar {
        margin-top: 6px;
        margin-bottom: 20px;
        /* Espacio horizontal buscador↔botón de filtro (misma fila). */
        gap: 10px;
    }
    /* Debe re-declararse AQUÍ (después de la regla .search-box de
       "controles flotantes", que pone min-width: 220px sin media query):
       a igual especificidad gana la última, y el min-width: 0 del bloque
       móvil de arriba perdía — a 320px el buscador no podía encoger y
       empujaba el botón de filtro a una segunda fila. */
    .search-box {
        min-width: 0;
    }
    body.brand-mode .brand-panel {
        margin-top: 4px;
    }
    body.brand-mode .brand-panel .timeline {
        padding-top: 4px;
    }
}

.empty-state-clear {
    display: block;
    margin: 10px auto 0;
}

/* ── Drawer edit CTA ── */
.cam-drawer-actions {
    /* margin-top:auto empuja el botón al fondo real del drawer cuando la
       ficha es corta (mismo truco que .ep-form con las acciones del panel
       de edición); padding-top garantiza el hueco de siempre cuando hay
       que hacer scroll y margin-top se queda en 0 al no sobrar espacio. */
    margin-top: auto;
    padding-top: 16px;
    text-align: center;
}
.cam-drawer-edit {
    width: 100%;
    min-height: 44px;
    font-weight: 600;
}
/* ── Sheet de filtros (inventario móvil) ── */
.filter-sheet {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay);
    display: flex;
    align-items: flex-end;
    justify-content: center;
}
.filter-sheet[hidden] {
    display: none;
}
.filter-sheet-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(4px);
}
.filter-sheet-panel {
    position: relative;
    width: 100%;
    max-width: 480px;
    max-height: min(85vh, 560px);
    background: var(--bg-raised);
    border-radius: 16px 16px 0 0;
    border: 1px solid var(--border);
    border-bottom: none;
    display: flex;
    flex-direction: column;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    animation: filterSheetIn 0.28s var(--ease-out);
}
@keyframes filterSheetIn {
    from { transform: translateY(100%); opacity: 0.6; }
    to { transform: translateY(0); opacity: 1; }
}
.filter-sheet-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Simétrico en vertical: con 16px arriba y 8px abajo, «Filtros» y la X
       quedaban 4px por debajo del centro óptico de su franja y el título se
       veía caído. Mismo alto total que antes (24px de padding), solo
       repartido a partes iguales. */
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
}
.filter-sheet-header h2 {
    font-size: 1.05rem;
    font-weight: 600;
}
/* Coletilla del encabezado: la usa la guía de modos («cómo se dispara cada
   cámara»), donde el título solo no dice para qué sirve la capa. Se va en
   pantallas estrechas, donde el título y la X ya llenan la franja. */
.filter-sheet-hint {
    margin-right: auto;
    padding-left: 8px;
    font-size: 0.78rem;
    color: var(--text-muted);
}
@media (max-width: 480px) {
    .filter-sheet-hint {
        display: none;
    }
}
/* Capa ancha: para contenido que son tarjetas y no una columna de campos. La
   guía de modos son dieciocho, agrupadas por marca. */
.filter-sheet--wide .filter-sheet-panel {
    max-width: 760px;
    max-height: min(88vh, 900px);
}
.filter-sheet--wide .filter-sheet-body {
    padding: 14px 16px 18px;
}
/* En escritorio se centra y cierra sus cuatro esquinas: un sheet pegado al
   borde inferior es el gesto de móvil (sube desde donde está el pulgar), pero
   con ratón y 900px de alto lo que se espera es un diálogo, como el de la
   comparativa de Cámaras. En móvil sigue siendo el sheet de siempre. */
@media (min-width: 769px) {
    .filter-sheet--wide {
        align-items: center;
    }
    .filter-sheet--wide .filter-sheet-panel {
        border-radius: 16px;
        border-bottom: 1px solid var(--border);
    }
}
.filter-sheet-body {
    overflow-y: auto;
    padding: 12px 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.filter-sheet-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.filter-sheet-label {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.filter-sheet-field .select-pill {
    width: 100%;
}
.filter-sheet-footer {
    display: flex;
    gap: 10px;
    padding: 12px 16px 16px;
    border-top: 1px solid var(--border);
}
.filter-sheet-footer .btn {
    flex: 1;
    min-height: 44px;
}

/* Toast: ver assets/css/components.css */

/* Movimiento reducido: los paneles deslizantes aparecen sin desplazamiento.
   Va al FINAL del archivo a propósito — las reglas base (.cam-drawer,
   .cam-scrim, .filter-sheet-panel) tienen la misma especificidad y en un
   empate gana la última en la cascada. Son los movimientos grandes (no los
   chips) los que más afectan a usuarios con sensibilidad vestibular. */
@media (prefers-reduced-motion: reduce) {
    .cam-drawer,
    .cam-scrim,
    .filter-sheet-panel {
        transition: none;
        animation: none;
    }
}

/* ============================================
   FOTO DE LA CÁMARA
   El inventario era texto puro: una colección de objetos, de un fotógrafo,
   sin una sola imagen. La foto va en la ficha (grande) y en la vista
   compacta (miniatura); NO en la tabla, que mide justo 1002px a 1280 de
   ventana y con una columna más volvería al scroll horizontal.
============================================ */

/* Ficha: la foto es lo primero, a sangre del ancho del panel. Proporción
   4/3 fija para que la ficha no dé un salto de altura mientras la imagen
   carga (el clásico CLS de las fichas con foto). */
.cam-photo {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    margin: 0 0 18px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--bg-raised);
    cursor: zoom-in;
}
.cam-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.cam-photo:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Visor a tamaño completo. Deliberadamente mínimo: una imagen y una X. El
   lightbox de Recetas (con comparador y contact sheet) es otro componente y
   vive en un módulo ES que aquí no está cargado. */
.cam-photo-viewer {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: max(16px, env(safe-area-inset-top)) 16px;
    background: rgba(var(--ac-overlay-black), 0.92);
    cursor: zoom-out;
}
.cam-photo-viewer img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: var(--radius-sm);
}
.cam-photo-viewer-close {
    position: absolute;
    top: max(12px, env(safe-area-inset-top));
    right: 12px;
}

/* Miniatura de la vista compacta. */
.cam-thumb {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    object-fit: cover;
    flex-shrink: 0;
    background: var(--bg-raised);
}
@media (max-width: 560px) {
    /* En el grid de dos filas de móvil, la miniatura ocupa las dos, entre la
       franja de color y los metadatos. */
    .brand-panel .cam-card-main .cam-thumb {
        grid-column: 2;
        grid-row: 1 / span 2;
        width: 44px;
        height: 44px;
    }
    .brand-panel .cam-card-main:has(.cam-thumb) {
        grid-template-columns: 3px auto 1fr auto;
    }
    .brand-panel .cam-card-main:has(.cam-thumb) .cam-meta {
        grid-column: 3;
    }
    .brand-panel .cam-card-main:has(.cam-thumb) .cam-tags {
        grid-column: 3;
    }
}

/* ============================================
   CARRETES (ficha lateral)
   Habla el mismo idioma que el resto de la ficha: el carrete que lleva
   dentro es una tarjeta `.cam-field` más —misma superficie, mismo rótulo en
   versalitas que «Velocidades» o «Estado»— y la acción va en la fila del
   título, no en un bloque propio. La primera versión apilaba tres cajas con
   tres tratamientos distintos y por eso desentonaba.
============================================ */

/* Título de sección con acción a la derecha (mismo patrón que .ep-tags-head
   en el editor). `.cam-sectit` es un margen suelto; aquí se convierte en fila. */
.cam-sectit--action {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

/* `.ac-btn-inline` vivía aquí, pero ya lo usan la ficha (Carretes) y el panel
   de edición (Foto): se ha mudado a components.css, con el resto del sistema
   de botones. */

/* El carrete cargado, como un campo más de la ficha. Hijo directo: desde que
   la tarjeta lleva dentro el desplegable «Ver detalle», un descendiente suelto
   alcanzaría también los valores de esa rejilla. */
.cam-field--roll > .cam-v {
    /* El nombre de la película puede ser largo («Kodak Professional Portra
       400»): se deja envolver en vez de recortarlo. */
    line-height: 1.35;
    text-wrap: pretty;
}
.cam-roll-meta {
    margin-top: 5px;
    font-family: "DM Mono", monospace;
    font-size: 0.7rem;
    color: var(--text-muted);
    letter-spacing: -0.01em;
}
.cam-empty--roll {
    margin: 0;
    padding: 10px 0 2px;
}

/* Historial: una lista, no un panel. Sin fondo ni borde propios — lo que
   separa las filas es el aire y una línea de 1px, igual que en el resto de
   la app. */
.ac-roll-history {
    margin-top: 14px;
}
/* Detalle de un carrete (`detalleDeCarrete` en rolls.js). Solo COLOCACIÓN: el
   desplegable reutiliza `.ac-roll-history-summary` y las tarjetas de dentro son
   la rejilla `.cam-fgrid` de la ficha, sin tocar. */
.ac-roll-detail {
    margin-top: 8px;
}
/* Las observaciones del carrete que está dentro de la cámara, en línea: es lo
   único que aportaba su «Ver detalle», retirado el 2026-08-03 por repetir el
   resto de la tarjeta. */
.ac-roll-loaded-notes {
    margin: 6px 0 0;
    font-size: 0.82rem;
}
.ac-roll-detail-grid {
    margin-top: 8px;
    gap: 8px;
}
.ac-roll-history-summary {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.7rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--text-faint);
    cursor: pointer;
    padding: 6px 0;
    list-style: none;
}
.ac-roll-history-summary::-webkit-details-marker {
    display: none;
}
.ac-roll-history-summary::after {
    content: "";
    width: 6px;
    height: 6px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg) translate(-1px, -1px);
    transition: transform 0.15s var(--ease-out);
}
.ac-roll-history[open] .ac-roll-history-summary::after {
    transform: rotate(-135deg) translate(-2px, -2px);
}
@media (hover: hover) {
    .ac-roll-history-summary:hover {
        color: var(--text-muted);
    }
}
.ac-roll-history-summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: var(--radius-sm);
}

.ac-roll-list {
    list-style: none;
    margin: 2px 0 0;
    padding: 0;
}
.ac-roll-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 0;
    border-top: 1px solid var(--border-subtle);
}
.ac-roll-item-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.ac-roll-item-film {
    font-size: 0.82rem;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ac-roll-item-meta {
    font-size: 0.68rem;
    color: var(--text-faint);
    font-family: "DM Mono", monospace;
}
.ac-roll-item-actions {
    display: flex;
    gap: 2px;
    flex-shrink: 0;
}
/* En escritorio, editar y borrar aparecen al posarse encima o al tabular:
   son acciones de mantenimiento y estaban compitiendo por la atención con el
   dato, que es lo que se viene a leer. En táctil no hay hover, así que se
   quedan siempre visibles. */
@media (hover: hover) {
    .ac-roll-item-actions {
        opacity: 0;
        transition: opacity 0.15s var(--ease-out);
    }
    .ac-roll-item:hover .ac-roll-item-actions,
    .ac-roll-item:focus-within .ac-roll-item-actions {
        opacity: 1;
    }
}
@media (prefers-reduced-motion: reduce) {
    .ac-roll-history-summary::after,
    .ac-roll-item-actions {
        transition: none;
    }
}

/* ============================================
           HISTORIAL DE SERVICIO (averías y taller)
        ============================================
   Sección hermana de la de carretes y con el mismo tratamiento: se apoya en
   `.cam-field` y `.cam-sectit--action`, que es el lenguaje que ya habla la
   ficha, en vez de traer un panel con estética propia. */
.ac-service-empty {
    margin: 0 0 4px;
    font-size: 0.78rem;
    color: var(--text-faint);
}

/* Las tres cifras que motivaron la tabla: gastado, visitas al taller y partes
   sin cerrar. Van arriba porque son la respuesta, y el historial de debajo es
   el detalle que la sostiene. */
.ac-service-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 2px 0 10px;
}
.ac-service-stat {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 6px 10px;
    background: var(--bg-surface);
    border-radius: var(--radius-sm);
    min-width: 0;
}
.ac-service-stat-v {
    font-family: "DM Mono", monospace;
    font-variant-numeric: tabular-nums;
    font-size: 0.95rem;
    font-weight: 500;
    color: var(--text-main);
    line-height: 1.1;
}
.ac-service-stat-k {
    font-size: 0.6rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-faint);
}

.ac-service-history {
    list-style: none;
    margin: 0;
    padding: 0;
}
.ac-service-item {
    position: relative;
    padding: 10px 0;
    border-top: 1px solid var(--border-subtle);
}
.ac-service-item-head {
    display: flex;
    align-items: center;
    gap: 8px;
    /* La insignia de estado no encoge; el texto de la avería sí, y se corta
       con puntos suspensivos antes que empujarla fuera de la fila. */
    min-width: 0;
}
.ac-service-fault {
    flex: 1;
    min-width: 0;
    font-size: 0.82rem;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.ac-service-item-head .ac-pill {
    flex-shrink: 0;
}
.ac-service-meta {
    margin-top: 3px;
    font-size: 0.68rem;
    color: var(--text-faint);
    font-family: "DM Mono", monospace;
    /* La línea cuenta el ciclo entero (detectado · llevada · volvió · coste) y
       en un móvil no cabe: envuelve en vez de recortarse, que aquí cada parte
       de la frase es un dato distinto. */
    line-height: 1.5;
}
.ac-service-work {
    margin-top: 4px;
    font-size: 0.76rem;
    color: var(--text-muted);
}
.ac-service-notes {
    margin-top: 2px;
    font-size: 0.72rem;
    color: var(--text-faint);
}
.ac-service-item-actions {
    position: absolute;
    top: 8px;
    right: 0;
    display: flex;
    gap: 2px;
}
/* Mismo criterio que en los carretes: en escritorio las acciones de
   mantenimiento aparecen al posarse encima o al tabular, para no competir con
   el dato. En táctil no hay hover y se quedan visibles. */
@media (hover: hover) {
    .ac-service-item-actions {
        opacity: 0;
        transition: opacity 0.15s var(--ease-out);
    }
    .ac-service-item:hover .ac-service-item-actions,
    .ac-service-item:focus-within .ac-service-item-actions {
        opacity: 1;
    }
}
@media (prefers-reduced-motion: reduce) {
    .ac-service-item-actions {
        transition: none;
    }
}

/* El formulario reparte los campos en dos columnas donde tiene sentido
   (fecha + dónde, fecha + coste). En móvil se apilan. */
.ac-service-cam {
    margin: 0 0 10px;
    font-size: 0.78rem;
    color: var(--text-faint);
}
.ac-service-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}
@media (max-width: 540px) {
    .ac-service-row {
        grid-template-columns: 1fr;
    }
}

/* ── El final del ciclo del carrete: laboratorio y escaneos ──────────────
   «Lo último que disparaste» va arriba del historial y no dentro: una colección
   de cámaras existe por las fotos que han hecho, no por la lista de carretes.
   Es el motivo de haber cerrado el ciclo (2026-07-31). */
.cam-field--last-shot .cam-roll-meta {
    margin-top: 3px;
}
.ac-roll-scans-cta {
    margin-top: 8px;
    /* `.ac-btn-inline` es un <button> en el resto de la app; aquí es un enlace,
       así que hay que devolverle el display y quitarle el subrayado. */
    display: inline-flex;
    text-decoration: none;
}
.ac-roll-scans-link {
    align-self: flex-start;
    margin-top: 3px;
    font-size: 0.68rem;
    color: var(--accent);
    text-decoration: none;
}
@media (hover: hover) {
    .ac-roll-scans-link:hover {
        text-decoration: underline;
    }
}
.ac-roll-scans-link:focus-visible,
.ac-roll-scans-cta:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
/* El botón de avanzar el ciclo es texto y no un icono: «Llevar a revelar» no
   tiene un glifo que se entienda, y es la acción que se quiere hacer fácil. */
.ac-roll-advance {
    white-space: nowrap;
}
.ac-roll-revelado {
    margin-top: 4px;
}

/* ============================================
           LA COLECCIÓN EN EL TIEMPO
        ============================================
   Segunda lectura del mismo hueco que ocupa la distribución por marca, no una
   sección más: el sitio de arriba del inventario es el más caro de la pantalla
   y ya se vació una vez (el dashboard de 7 KPIs, ver docs/PRODUCT.md). */
.chart-section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
}
.chart-section-head .chart-section-title {
    margin-bottom: 0;
}
/* Usa el control segmentado del sistema (.ac-segment): así el conmutador se
   lee igual que el de vista tabla/marca del toolbar y no como dos textos
   sueltos, que es como quedaba con un contenedor propio. */
.chart-view-switch {
    flex-shrink: 0;
}

.timeline-row {
    /* La pista de marcas necesita todo el ancho que le sobre a la etiqueta de
       la década y al recuento, así que no reparte en tres columnas fijas como
       `.chart-row`: la del medio crece. */
    align-items: flex-start;
}
.timeline-decade {
    /* La década va en la misma columna que la marca del otro gráfico, para que
       cambiar de vista no mueva nada de sitio. */
    font-family: "DM Mono", monospace;
    font-variant-numeric: tabular-nums;
    padding-top: 3px;
}
.timeline-track {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    min-height: 24px;
    min-width: 0;
}
/* Una década sin cámaras se deja vacía a propósito: el hueco entre 1986 y 1995
   es información, y saltárselo dibujaría una colección continua que no existe.
   Se atenúa la etiqueta para que se lea como silencio y no como un fallo. */
.timeline-row--vacia .timeline-decade {
    opacity: 0.4;
}
.timeline-row--vacia .timeline-track::before {
    content: "";
    align-self: center;
    width: 100%;
    height: 1px;
    background: var(--border-subtle);
}

/* Una marca por cámara y no una barra: con 26 cámaras lo interesante no es
   «cuántas» sino «cuáles». Cada marca abre su ficha. */
.timeline-mark {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    padding: 3px 9px 3px 8px;
    background: var(--bg-hover);
    border: 0;
    border-left: 2px solid var(--dot-color, var(--accent));
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    color: var(--text-muted);
    font-size: 0.72rem;
    line-height: 1.4;
    cursor: pointer;
    text-align: left;
    transition: background 0.15s var(--ease-out), color 0.15s var(--ease-out);
}
.timeline-mark-year {
    font-family: "DM Mono", monospace;
    font-variant-numeric: tabular-nums;
    font-size: 0.68rem;
    color: var(--text-faint);
}
.timeline-mark-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
@media (hover: hover) {
    .timeline-mark:hover {
        background: var(--bg-surface);
        color: var(--text-main);
    }
}
.timeline-mark:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.timeline-mark:active {
    transform: scale(0.97);
}
@media (prefers-reduced-motion: reduce) {
    .timeline-mark {
        transition: none;
    }
    .timeline-mark:active {
        transform: none;
    }
}

.timeline-sin-ano,
.timeline-empty {
    margin: 10px 0 0;
    font-size: 0.7rem;
    color: var(--text-faint);
}

/* En móvil la etiqueta de la década pasa arriba: con 80px reservados a la
   izquierda, a las marcas les quedaban dos palabras por línea. */
@media (max-width: 768px) {
    .timeline-row {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "decada recuento"
            "pista  pista";
        gap: 4px 10px;
    }
    .timeline-decade {
        grid-area: decada;
        text-align: left;
        padding-top: 0;
    }
    .timeline-track {
        grid-area: pista;
    }
    .timeline-row .chart-count {
        grid-area: recuento;
    }
}

/* ── Puente analógico ↔ digital ─────────────────────────────────────────
   La receta que imita esta película en las cámaras digitales. Solo aparece en
   6 de las 13 películas: las demás emulan carretes que no están en el catálogo
   o no imitan ninguno en concreto, y enlazar «lo más parecido» sería
   inventárselo. Va al final de la ficha, separado, porque cambia de tema:
   arriba se habla de la película y aquí de cómo hacerla en digital. */
.ac-roll-film-digital {
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--border-subtle);
}
.ac-roll-film-digital-k {
    display: block;
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 3px;
}
.ac-roll-film-digital-v {
    margin: 0;
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.45;
}

/* ============================================
   GUÍA DE MODOS — lo de dentro (2026-08-22)
   ============================================
   La guía del inventario ya NO tiene aspecto propio: es la vista compacta
   agrupada por marca, con sus mismas clases (`.brand-panel`, ver
   inventory-timeline.css y el modificador `--modes` de ese archivo). Lo pidió
   así el usuario, y es lo que dice AGENTS.md: primero el componente que ya
   existe.

   Lo que queda aquí es solo lo que va DENTRO de la tarjeta desplegada: la
   lista de modos. (Estuvo también en la ficha lateral de cada cámara hasta que
   el usuario la quitó de ahí — la ficha es el estado de SU unidad y los modos
   son del modelo.)

   Filas finas con su línea de 1px, como el historial de carretes
   (`.ac-roll-item`), y el icono del tipo en su propia columna: alineados en
   vertical se leen como una columna de símbolos y se busca «el manual» sin
   leer una palabra. Antes de esto fueron tarjetas `.cam-field`, y el usuario
   las rechazó por el mismo motivo por el que ya no están: cajas con borde,
   fondo y relleno para tres líneas de texto. */
.cam-modes-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.cam-mode {
    display: grid;
    grid-template-columns: 20px minmax(0, 1fr);
    gap: 10px;
    padding: 9px 0;
    border-top: 1px solid var(--border-subtle);
}
.cam-mode:first-child {
    border-top: none;
}
.cam-mode-icon {
    display: flex;
    justify-content: center;
    padding-top: 2px;
    color: var(--text-faint);
}
.cam-mode-icon svg {
    width: var(--ac-icon-sm);
    height: var(--ac-icon-sm);
}
.cam-mode-name {
    margin: 0;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-faint);
}
/* «Cómo se pone»: la frase que se lee con la cámara en la mano, con el peso
   de un valor de la ficha y no el del cuerpo de texto. */
.cam-mode-how {
    margin: 2px 0 0;
    font-size: 0.875rem;
    line-height: 1.45;
    text-wrap: pretty;
    /* La columna del inventario es ancha y un renglón de ese largo se lee mal:
       se corta donde lo cortaría un libro. */
    max-width: 74ch;
}
/* «Qué hace»: contexto, un escalón por debajo. */
.cam-mode-what {
    margin: 3px 0 0;
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.5;
    text-wrap: pretty;
    max-width: 78ch;
}
.cam-mode-summary {
    max-width: 78ch;
    margin: 0 0 2px;
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.5;
    text-wrap: pretty;
}
/* Los avisos: lo que se te va a olvidar. La barra de acento es lo único con
   color de todo el bloque, para que se vean sin gritar. */
.cam-mode-notes {
    list-style: none;
    margin: 10px 0 0;
    padding: 2px 0 2px 10px;
    border-left: 2px solid rgba(var(--accent-rgb), 0.35);
    display: grid;
    gap: 6px;
}
.cam-mode-notes li {
    max-width: 78ch;
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.5;
    text-wrap: pretty;
}

/* El aviso del carrete olvidado, dentro del campo «En la cámara» de la ficha.
   Misma barra de acento que los avisos de los modos (`.cam-mode-notes`), pero
   en amarillo: allí es «esto conviene saberlo» y aquí es «esto va contigo». */
.cam-roll-olvido {
    margin: 8px 0 0;
    padding-left: 10px;
    border-left: 2px solid var(--warn-tx, #ffd97a);
    font-size: 0.78rem;
    line-height: 1.5;
    color: var(--warn-tx, #ffd97a);
    text-wrap: pretty;
}

/* ── QUÉ PONGO PARA CADA FOTO ──────────────────────────────────────────────
   La segunda mitad de la ficha (2026-08-25): la guía de modos dice qué hace
   cada dial y esto dice qué poner ante una escena concreta. Va DEBAJO de los
   modos y de sus avisos, con el mismo ritmo de filas finas separadas por 1px
   —`.cam-mode`, y antes de eso `.ac-roll-item`— para que la ficha entera se
   lea como una sola cosa y no como dos bloques pegados.
   Sin rejilla de icono: aquí el icono no está a la izquierda de la fila sino
   dentro de la pastilla del modo, que es donde significa algo. */
.cam-sits {
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--border-subtle);
}
.cam-sits-title {
    margin: 0 0 4px;
}
.cam-sits-list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.cam-sit {
    padding: 9px 0;
    border-top: 1px solid var(--border-subtle);
}
.cam-sit:first-child {
    border-top: none;
}
/* Nombre y modo en la misma línea, y el modo empujado a la derecha: leídas en
   vertical, la columna de la derecha contesta sola a «¿y qué pongo?». */
.cam-sit-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px 10px;
}
.cam-sit-name {
    margin: 0;
    font-size: 0.875rem;
    font-weight: 600;
}
.cam-sit-mode {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: 0 0 auto;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--accent);
}
.cam-sit-mode-icon {
    width: var(--ac-icon-sm);
    height: var(--ac-icon-sm);
    flex: 0 0 auto;
}
/* Los tres números, en la mono del resto de datos duros de la app (nº de
   serie, coste de revelado): son lo que se copia al dial, así que van con la
   forma de un dato y no la de una frase. */
.cam-sit-nums {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 8px;
    margin: 5px 0 0;
}
.cam-sit-num {
    font-family: "DM Mono", monospace;
    font-size: 0.78rem;
    color: var(--text-main);
    background: rgba(var(--ac-overlay-white), 0.06);
    border-radius: var(--radius-sm);
    padding: 2px 7px;
}
.cam-sit-when,
.cam-sit-watch {
    margin: 4px 0 0;
    max-width: 78ch;
    line-height: 1.5;
    text-wrap: pretty;
}
.cam-sit-when {
    font-size: 0.78rem;
    color: var(--text-muted);
}
.cam-sit-watch {
    font-size: 0.75rem;
    color: var(--text-faint);
}
/* Fuera de alcance: la fila se apaga entera y los números desaparecen —no
   sirven de nada aquí— y en su sitio va qué hacer en su lugar. Es la misma
   idea que `.ac-pill--empty` en los chips de filtro que dan cero: no se
   esconde, se apaga. */
.cam-sit--no .cam-sit-name {
    color: var(--text-muted);
}
.cam-sit-fuera {
    margin: 5px 0 0;
    max-width: 78ch;
    font-size: 0.78rem;
    line-height: 1.5;
    text-wrap: pretty;
    color: var(--text-muted);
    padding-left: 10px;
    border-left: 2px solid var(--border-subtle);
}
/* «No consta»: ni verde ni rojo. La cámara casi seguro puede y lo que falta es
   el dato, así que se avisa con el mismo acento que los avisos de los modos. */
.cam-sit-duda {
    margin: 5px 0 0;
    max-width: 78ch;
    font-size: 0.75rem;
    line-height: 1.5;
    text-wrap: pretty;
    color: var(--text-muted);
    padding-left: 10px;
    border-left: 2px solid rgba(var(--accent-rgb), 0.35);
}
