Files
bachir d2f3179f01 Import de relevé bancaire CSV en libre-service (/lignes/importer-releve)
Chaque transaction du relevé devient une ligne « à trier » : type autre,
répartition vide (liseré rouge existant via field_ecart), tag field_flag
« IMP AAMMJJ » (liseré ambre + filtre existants), client rapproché par
mots (ClientMatcher : séquence contiguë, sinon mot significatif unique --
jamais en sous-chaîne, jamais auto-créé).

- src/Import/ : CsvReleveParser (ISO-8859-1 confirmé, en-tête strict,
  fgetcsv avec escape '' explicite -- dépréciation PHP 8.4, rejets
  propres avec n° de ligne), ReleveImportBatch (Batch API par lots de
  25, dédoublonnage COMPTÉ par empreinte field_import_fitid -- max(0,
  k−m) importe les vrais doublons légitimes et dédoublonne à travers
  des fichiers qui se chevauchent --, totaux de contrôle au centime
  sur la page de résultat, résumé en tempstore privé).
- ReleveUploadForm : upload private://releves (fichier conservé +
  usage, hors de portée du cron), parse en validateForm(), batch,
  redirection vers la page de résultat.
- field_montant_releve : référence bancaire immuable, écrite une fois
  à l'import et jamais par presave ; affichée en TEXTE sous Montant
  TTC (widget remplacé par un #type item -- un item ne soumet rien et
  extractFormValues() saute le champ sans valeur soumise, la valeur
  survit donc à chaque save) ; masquée sur les lignes sans montant.
- SkipValidationContext : contournement du contrôle de répartition
  requête-scopé (ferme le trou de concurrence de l'ancien state
  global, AUDIT-2026-09-09 §2.2) -- presave honore le service (state
  gardé pour compat), updateType/updateField basculent dessus.
- Permissions (AUDIT §2.2 priorité 1) : access figli ledger sur toutes
  les routes du module + autocomplete (RouteSubscriber), import
  réservé Éditeur/Admin, access content retiré du rôle Authenticated
  (config/sync re-exportée pour les 4 rôles).
- Gin : hook_gin_ignore_sticky_form_actions() -- sans ça, le bouton
  Importer partait dans la barre sticky du chrome masqué.
- install : 8011 champs, 8012 index sur les empreintes, 8013/8014
  montant_releve sur le formulaire sous le TTC (poids renumérotés).
- /lignes : boutons + Ajouter / Importer / Historique dans le footer
  sticky (compacts), footer colspan dès la première colonne, badges de
  signalement qui reviennent à la ligne au lieu de déborder.
2026-09-09 14:58:36 +02:00

769 lines
24 KiB
CSS

/* Self-contained colors (not relying on Gin CSS custom properties, which
don't reliably resolve on this route) + explicit dark-mode overrides,
since Gin's dark mode (html.gin--dark-mode) is enabled by default here. */
#figli-home-app {
--figli-bg: #ffffff;
--figli-bg-alt: #f5f6f8;
--figli-text: #1a1a1a;
--figli-text-light: #6b7280;
--figli-border: #dcdee2;
--figli-error: #c9312b;
--figli-positive: #1a7f37;
--figli-warning: #b8860b;
--figli-col-hover: rgba(15, 23, 42, 0.05);
font-family: Inter, -apple-system, sans-serif;
/* Bottom cut to a third (top left alone) -- with the outer page
scroll already eliminated (see hide-admin-chrome.css's main.page-
content override), the full 1rem below the table was just dead
space past the last visible row. */
margin: 1rem 0 0.33rem;
color: var(--figli-text);
}
html.gin--dark-mode #figli-home-app {
--figli-bg: #232426;
--figli-bg-alt: #2c2d30;
--figli-text: #e8e9ea;
--figli-text-light: #a1a5ab;
--figli-border: #3d3e42;
--figli-error: #ff6b6b;
--figli-positive: #4ade80;
--figli-warning: #f0b429;
--figli-col-hover: rgba(255, 255, 255, 0.07);
}
#figli-home-app .figli-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1.25rem;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: var(--figli-bg-alt);
border: 1px solid var(--figli-border);
border-radius: 6px;
color: var(--figli-text);
}
#figli-home-app .figli-toolbar label {
display: flex;
flex-direction: column;
font-size: 0.75rem;
font-weight: 600;
color: var(--figli-text-light);
gap: 0.15rem;
}
#figli-home-app .figli-toolbar select {
font-size: 0.85rem;
padding: 0.2rem 0.4rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
}
/* Each filter control + its "✕ effacer" button, side by side. */
#figli-home-app .figli-filter-row {
display: flex;
align-items: center;
gap: 0.25rem;
}
#figli-home-app .figli-filter-clear {
background: transparent;
border: none;
color: var(--figli-text-light);
cursor: pointer;
font-size: 0.8rem;
line-height: 1;
padding: 0.15rem;
}
#figli-home-app .figli-filter-clear:hover {
color: var(--figli-error);
}
/* Compte/Type filters: a native <select multiple> can't collapse to a
single-line trigger and expand as an overlay -- it's either a
single-line dropdown, or an always-open multi-row list box. Custom
dropdown instead: a trigger showing the current selection, and a
checkbox list overlay shown on hover (pure CSS, no open/close state
needed) -- checkboxes rather than ctrl/shift+click, but same result
(several comptes/types at once) with an even more discoverable
interaction (no keyboard modifier to know about).*/
#figli-home-app .figli-multiselect {
position: relative;
}
#figli-home-app .figli-multiselect-trigger {
font-size: 0.85rem;
padding: 0.2rem 0.4rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
width: 9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
}
#figli-home-app .figli-multiselect-arrow {
float: right;
color: var(--figli-text-light);
}
#figli-home-app .figli-multiselect-panel {
display: none;
position: absolute;
top: 100%;
left: 0;
z-index: 20;
min-width: 100%;
white-space: nowrap;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
padding: 0.3rem 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
#figli-home-app .figli-multiselect:hover .figli-multiselect-panel {
display: block;
}
#figli-home-app .figli-multiselect-option {
display: flex !important;
flex-direction: row !important;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
font-weight: 400;
padding: 0.15rem 0;
}
#figli-home-app .figli-client-input {
font-size: 0.85rem;
padding: 0.2rem 0.4rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
width: 12rem;
}
#figli-home-app .figli-checkbox {
flex-direction: row !important;
align-items: center;
gap: 0.35rem !important;
}
#figli-home-app .figli-count {
margin-left: auto;
font-size: 0.8rem;
color: var(--figli-text-light);
white-space: nowrap;
}
#figli-home-app .figli-table-wrap {
/* Fixed columns below are sized to always fit -- hidden (not auto) so
there's never a horizontal scrollbar to fight the vertical one for
layout space. With both set to auto, the browser has to guess
whether a vertical scrollbar will appear before it can lay out the
table's width:100%, and getting that guess wrong understates the
available width by one scrollbar's worth, which was enough to tip
this from "just barely fits" into "needs to scroll horizontally
too" -- and any actual residual overflow (a column pushed a few px
over by content this doesn't attempt to break) is silently clipped
here instead of surfacing a scrollbar for it. */
overflow-x: hidden;
border: 1px solid var(--figli-border);
border-radius: 6px;
/* Not a flat 75vh -- title/nav + toolbar (with its filter row) above
this add up to a roughly fixed ~16rem regardless of viewport
height, so a percentage-of-viewport max-height only fits by
coincidence at one particular window size and silently reintroduces
the outer page scroll (on top of this element's own) at any other,
as toolbar filters were added over time and pushed that fixed part
past what 75vh left room for. Subtracting the fixed part directly
keeps this sized to exactly the remaining space instead. */
max-height: calc(100vh - 16rem);
overflow-y: auto;
}
/* table-layout: fixed -- auto layout was recomputing every column's
width from whatever happened to be currently loaded, so the table
visibly reflowed (and drifted wider than the viewport, forcing
horizontal scroll) every time loadOlder()/loadNewer() brought in rows
with different content. Fixed layout locks each column to the widths
set below regardless of content, so the table's total width never
changes -- no more horizontal scroll, and loadOlder()'s scroll
compensation (which assumes the height delta after prepending is
*only* the new rows' own height) is now accurate again, since existing
rows no longer reflow when new ones are added. */
#figli-home-app table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
background: var(--figli-bg);
color: var(--figli-text);
font-size: 0.8rem;
}
#figli-home-app th,
#figli-home-app td {
/* border-box -- otherwise each column's percentage width (fixed
layout, above) sets only the content box, and this cell's own
padding/border get added on top of it, so 18 columns' worth of
padding quietly pushes the table wider than 100% again. */
box-sizing: border-box;
text-align: left;
padding: 0.3rem 0.6rem;
border-bottom: 1px solid var(--figli-border);
color: var(--figli-text);
}
#figli-home-app thead th {
position: sticky;
top: 0;
background: var(--figli-bg-alt);
color: var(--figli-text);
font-weight: 600;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.02em;
z-index: 2;
}
#figli-home-app td.amount,
#figli-home-app th.amount {
text-align: right;
font-variant-numeric: tabular-nums;
}
#figli-home-app th:nth-child(6),
#figli-home-app td.figli-libelle {
width: 10%;
white-space: normal;
}
#figli-home-app th:nth-child(7),
#figli-home-app td.figli-flag-cell {
width: 6%;
white-space: normal;
}
#figli-home-app tr.figli-group-row td {
background: var(--figli-bg-alt);
color: var(--figli-text);
font-weight: 700;
position: sticky;
top: 2rem;
z-index: 1;
}
#figli-home-app tfoot {
position: sticky;
bottom: 0;
/* Higher than thead (2) and the sticky group-row headers (1) -- without
this, tfoot had no explicit z-index (auto), so a group-row header
scrolling underneath it painted on top instead of behind. */
z-index: 3;
}
#figli-home-app tr.figli-totals-row td {
background: var(--figli-bg-alt);
color: var(--figli-text);
font-weight: 700;
border-top: 2px solid var(--figli-border);
border-bottom: none;
}
/* "+ Ajouter une ligne" + "Importer un relevé" live in the sticky footer
now (they used to lead the toolbar and crowd its filter row). Compact
overrides for Gin's .button, which is sized for full admin forms --
way too big inside a dense totals row. Size-only overrides (no
colors): Gin's own light/dark button palettes keep applying. */
#figli-home-app tr.figli-totals-row .button {
display: inline-block;
margin: 0 0.4rem 0 0;
padding: 0.15rem 0.55rem;
font-size: 0.72rem;
line-height: 1.4;
vertical-align: middle;
border-radius: 4px;
box-shadow: none;
}
#figli-home-app td.figli-solde-crediteur {
color: var(--figli-positive);
}
#figli-home-app td.figli-solde-debiteur {
color: var(--figli-error);
}
/* Footer totals row sets its own `color` (higher specificity: tr.class +
td beats a lone td.class) which otherwise overrides the solde colors
above -- repeat them scoped to the row so red/green still wins there. */
#figli-home-app tr.figli-totals-row td.figli-solde-crediteur {
color: var(--figli-positive);
}
#figli-home-app tr.figli-totals-row td.figli-solde-debiteur {
color: var(--figli-error);
}
/* Single colored + bold anchor per row (Montant HT only) so entrées vs
sorties are scannable at a glance -- everything else in the row (TTC,
the 8 compte columns) stays neutral, not a red/green garland. */
#figli-home-app td.figli-montant-positif {
color: var(--figli-positive);
font-weight: 700;
}
#figli-home-app td.figli-montant-negatif {
color: var(--figli-error);
font-weight: 700;
}
/* Écart is called out by the Écart column itself (bold, red or green
depending on sign -- see below) -- no separate row-level treatment
needed. Répartition > Montant HT (positive écart) means the accounts
were credited more than what's actually owed for this line, the same
"more money than expected" sense .figli-montant-positif already
marks green elsewhere; the reverse (negative) stays the usual
red/error color. */
#figli-home-app td.figli-ecart {
color: var(--figli-error);
font-weight: 700;
}
#figli-home-app td.figli-ecart-positif {
color: var(--figli-positive);
font-weight: 700;
}
/* Signalé rows: a thick amber accent bar on the left edge. */
#figli-home-app tr.figli-flag-row td:first-child {
box-shadow: inset 4px 0 0 var(--figli-warning);
}
#figli-home-app .figli-flag-badge {
display: inline-block;
padding: 0.05rem 0.4rem;
margin: 0 0.2rem 0.15rem 0;
border-radius: 8px;
font-size: 0.68rem;
font-weight: 600;
background: color-mix(in srgb, var(--figli-warning) 15%, transparent);
color: var(--figli-warning);
/* The Signalement column is narrow (6%) -- a badge must wrap inside
the cell instead of overflowing into the neighboring column. The
cell itself already allows wrapping (.figli-flag-cell); this makes
the badge wrap too, including single long tokens (anywhere) and
within its own padding box (max-width + border-box). */
box-sizing: border-box;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
}
/* Column highlight to pair with the row hover, forming a crosshair over
the hovered cell. box-shadow (not background) so it layers on top of
whatever the cell already has -- sticky header/footer backgrounds,
error-row outlines -- instead of overwriting them. */
#figli-home-app td.figli-col-hover,
#figli-home-app th.figli-col-hover {
box-shadow: inset 0 0 0 9999px var(--figli-col-hover);
}
#figli-home-app .figli-badge {
display: inline-block;
padding: 0.1rem 0.45rem;
border-radius: 10px;
font-size: 0.68rem;
font-weight: 600;
white-space: nowrap;
}
#figli-home-app .type-entree { background: #1a7f371a; color: var(--figli-positive); }
#figli-home-app .type-charge { background: #6b72801a; color: var(--figli-text-light); }
#figli-home-app .type-versement { background: #d97a0a1a; color: #d97a0a; }
#figli-home-app .type-achat { background: #1d4ed81a; color: #3b6fe0; }
#figli-home-app .type-hebergement { background: #0e91821a; color: #0e9182; }
#figli-home-app .type-ouverture { background: #7c3aed1a; color: #9061f0; }
#figli-home-app .type-sous_traitant { background: #c9312b1a; color: #c9312b; }
#figli-home-app .type-salaire_stage { background: #0891b21a; color: #0891b2; }
#figli-home-app .type-charges_local_pro { background: #65a30d1a; color: #65a30d; }
#figli-home-app .type-autre { background: #6b72801a; color: var(--figli-text-light); }
/* Click-to-edit type badge -- swaps for a native <select> in place
(figli-type-select below), no modal needed for this one field. */
#figli-home-app .figli-badge-editable {
cursor: pointer;
border: 1px solid transparent;
}
#figli-home-app .figli-badge-editable:hover {
border-color: currentColor;
}
#figli-home-app .figli-type-select {
font-size: 0.72rem;
padding: 0.05rem 0.2rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
}
/* Client/Facture/Libellé: click-to-edit like the type badge above, but
plain text rather than a pill -- a dotted underline is enough of an
affordance without implying a fixed set of choices the way the type
badge's pill shape does. */
/* display: block + min-height (not just the default inline text-only
box) so the whole cell is a forgiving click target even when empty --
the "—" placeholder alone is a tiny, easy-to-miss target otherwise. */
#figli-home-app .figli-editable-cell {
display: block;
min-height: 1.2em;
cursor: pointer;
border-bottom: 1px dotted transparent;
}
#figli-home-app .figli-editable-cell:hover {
border-bottom-color: var(--figli-text-light);
}
#figli-home-app .figli-inline-input {
font-size: 0.8rem;
padding: 0.1rem 0.3rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
width: 100%;
box-sizing: border-box;
}
#figli-home-app .figli-note {
color: var(--figli-text-light);
font-weight: 400;
font-size: 0.75rem;
}
/* One column per icon (link, edit) rather than one shared cell -- with a
shared cell, text-align: center puts a lone button (rows with no link
icon) in a different spot than a row with both, so the edit pencil
never lined up from row to row. Separate columns line up by
construction, and get a tight, icon-sized width instead of the
table's default 0.6rem horizontal cell padding. */
#figli-home-app td.actions-col,
#figli-home-app th.actions-col {
width: 3%;
padding: 0.2rem 0.1rem;
text-align: center;
white-space: nowrap;
}
/* Column widths for the fixed layout above -- sized so the widest
realistic content (longest client/type label, an 8-figure amount)
fits without pushing the table past 100%, while the two free-text
columns (Client, Libellé/Détail) get most of the remaining room.
Every column is a percentage (including actions-col/Date above,
deliberately not rem) -- mixing units meant the rem columns' pixel
width was added *on top of* the percentage budget instead of coming
out of it, silently pushing the table a few pixels past 100% and
reintroducing the horizontal scrollbar this is meant to avoid.
Percentages intentionally sum to a little under 100%: table-layout:
fixed treats them as relative weights, not a hard budget, so slightly
under leaves headroom rather than risking every column getting
scaled down to fit. */
#figli-home-app th:nth-child(2),
#figli-home-app td:nth-child(2) {
width: 5%;
white-space: nowrap;
}
#figli-home-app th:nth-child(3),
#figli-home-app td:nth-child(3) {
width: 10%;
}
#figli-home-app th:nth-child(4),
#figli-home-app td:nth-child(4) {
width: 7%;
}
#figli-home-app th:nth-child(5),
#figli-home-app td:nth-child(5) {
width: 5%;
white-space: nowrap;
}
#figli-home-app .amount:not(.compte-col) {
width: 6.2%;
white-space: nowrap;
}
/* HT/TTC narrower than the general .amount rule above (still used by
Écart) -- shorter header text (HT/TTC vs. the old Montant HT/Montant
TTC) and TVA splitting the row between them means neither needs as
much room. TVA itself narrower still: "12,64 %" is the longest
realistic value (see figli_compta_ledger_update_8005()'s docblock on
backfilled non-round rates), well short of an 8-figure amount.
`.amount` doubled up in the selector (not just .figli-ht-col alone):
:not(.compte-col) in the rule above counts toward specificity same as
a real class, so a single-class selector here would lose to it. */
#figli-home-app .amount.figli-ht-col,
#figli-home-app .amount.figli-ttc-col {
width: 5%;
}
/* Cotisation diffuseur URSSAF (1,1%) -- blank for every non-"Entrée
client" row (see figli_compta_ledger_node_presave()), same width
budget as HT/TTC since it's a real € amount, not a short rate like
TVA. */
#figli-home-app .amount.figli-cotisation-col {
width: 5%;
}
#figli-home-app .amount.figli-tva-col {
width: 3.5%;
}
/* Non-official TVA rate marker -- a thick red underline on the cell
itself rather than a small badge/dot (tried first, not visible
enough): the column is only ~3.5% wide, barely enough for
"12,64 %" itself, so a border under the whole cell reads clearly at
a glance without needing extra horizontal room a text/icon badge
would need. Tooltip (title attribute in the twig template) carries
the actual explanation. */
#figli-home-app td.figli-tva-nonofficial {
border-bottom: 3px solid var(--figli-error);
}
#figli-home-app .amount.compte-col {
width: 4.3%;
white-space: nowrap;
}
/* Buttons stay inline (not flex) inside the <td> -- display: flex on a
table cell breaks the table's own column-width calculation (observed:
a ~55px gap opening up between this column and the next, and the
buttons rendering outside their cell's actual boundary). */
#figli-home-app .figli-edit-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.4rem;
height: 1.4rem;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--figli-text-light);
cursor: pointer;
vertical-align: middle;
}
#figli-home-app .figli-edit-btn:hover {
background: var(--figli-bg-alt);
border-color: var(--figli-border);
color: var(--figli-text);
}
/* Reconciliation badge on entrée rows: répartition (money owed) vs the
combined répartition of every linked sortie (money actually paid out). */
#figli-home-app .figli-recon-badge {
display: inline-block;
margin-left: 0.4rem;
padding: 0.05rem 0.4rem;
border-radius: 10px;
font-size: 0.68rem;
font-weight: 600;
white-space: nowrap;
background: #1a7f371a;
color: var(--figli-positive);
}
/* Not every badge using this class does something on click (the
ouverture/clôture écart badge is purely informational) -- only show
the pointer cursor where a click actually goes somewhere. */
#figli-home-app .figli-recon-badge.is-clickable {
cursor: pointer;
}
/* Still green, not orange -- it's money owed, not a warning, and
green-but-darker reads as "related to, but not yet, Lié" (the default
badge green above) rather than as an unrelated anomaly color. */
#figli-home-app .figli-recon-badge.is-reste {
background: #1665341a;
color: #166534;
}
#figli-home-app .figli-recon-badge.is-anomalie {
background: #c9312b1a;
color: var(--figli-error);
}
#figli-home-app .figli-clear-drilldown {
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
}
/* Entrée + sorties liées drill-down: a modal overlay rather than
replacing the main table's rows in place, so opening/closing it never
disturbs the main table's scroll position. */
#figli-home-app .figli-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
#figli-home-app .figli-modal {
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 8px;
width: 96vw;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
#figli-home-app .figli-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--figli-border);
}
#figli-home-app .figli-modal-header h3 {
margin: 0;
font-size: 0.95rem;
font-weight: 700;
}
#figli-home-app .figli-modal-close {
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--figli-text-light);
cursor: pointer;
font-size: 0.9rem;
padding: 0.15rem 0.45rem;
}
#figli-home-app .figli-modal-close:hover {
background: var(--figli-bg-alt);
border-color: var(--figli-border);
color: var(--figli-text);
}
/* Signalement modal -- much smaller than the entrées/sorties drill-down
above (a short list of tags, not a table), so it gets its own narrow
width instead of the 96vw default. */
#figli-home-app .figli-flag-modal {
width: 24rem;
max-width: 90vw;
}
#figli-home-app .figli-flag-modal-body {
padding: 0.75rem 1rem 1rem;
}
#figli-home-app .figli-flag-modal-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.35rem 0;
border-bottom: 1px solid var(--figli-border);
}
#figli-home-app .figli-flag-remove {
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--figli-text-light);
cursor: pointer;
font-size: 0.8rem;
padding: 0.1rem 0.4rem;
flex: none;
}
#figli-home-app .figli-flag-remove:hover {
background: var(--figli-bg-alt);
border-color: var(--figli-border);
color: var(--figli-error);
}
#figli-home-app .figli-flag-modal-add {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
#figli-home-app .figli-flag-modal-add input {
flex: 1;
font-size: 0.85rem;
padding: 0.3rem 0.5rem;
border: 1px solid var(--figli-border);
border-radius: 4px;
background: var(--figli-bg);
color: var(--figli-text);
}
#figli-home-app .figli-modal-body {
overflow-y: auto;
overflow-x: hidden;
padding: 0;
}
#figli-home-app .figli-modal-body table {
font-size: 0.8rem;
table-layout: fixed;
width: 100%;
/* Only a handful of rows in this view -- wrap instead of the main
table's nowrap, so 14 columns fit the modal width without ever
needing horizontal scroll. */
white-space: normal;
}
#figli-home-app .figli-modal-body thead th {
position: sticky;
top: 0;
}
/* table-layout: fixed otherwise spreads all 16 columns evenly, squeezing
the libellé text into the same width as an empty compte column --
give the columns that actually carry content (client/type/libellé)
proportionally more room, comptes/montants less. */
#figli-home-app .figli-modal-body th:nth-child(1),
#figli-home-app .figli-modal-body td:nth-child(1) {
width: 2rem;
}
#figli-home-app .figli-modal-body th:nth-child(2),
#figli-home-app .figli-modal-body td:nth-child(2) {
width: 4.5rem;
}
#figli-home-app .figli-modal-body th:nth-child(3),
#figli-home-app .figli-modal-body td:nth-child(3) {
width: 9%;
}
#figli-home-app .figli-modal-body th:nth-child(4),
#figli-home-app .figli-modal-body td:nth-child(4) {
width: 8%;
}
#figli-home-app .figli-modal-body th:nth-child(5),
#figli-home-app .figli-modal-body td.figli-libelle {
width: 24%;
min-width: 0;
max-width: none;
}
#figli-home-app .figli-modal-body .amount.compte-col {
width: 4.5%;
}
#figli-home-app .figli-modal-body .amount:not(.compte-col) {
width: 7%;
}
/* Sliding-window edge markers (IntersectionObserver targets) -- kept
short so they don't add visible dead space when idle, tall enough
(min-height) to reliably intersect the observer's root margin. */
#figli-home-app tr.figli-sentinel-row td {
padding: 0.3rem 0.6rem;
min-height: 1.5rem;
text-align: center;
color: var(--figli-text-light);
font-size: 0.75rem;
font-style: italic;
}
#figli-home-app .figli-error {
background: #fde8e8;
border: 1px solid #f4a3a3;
color: #7a1a1a;
padding: 0.75rem 1rem;
border-radius: 6px;
}
/* Dismissible, doesn't replace the table like the top-level fetch error
does -- a failed inline type change is a small hiccup, not a reason to
hide everything that's already loaded. */
#figli-home-app .figli-inline-error {
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}