diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3625865..d31e3b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- **Exile-aware nation filter (web UI).** Filtering the collection by nation now
+ includes that nation's exile cards by default — cards of another nation that
+ can be played in its decks (for example the Polish *T-34 76 PL* under Soviet),
+ matching the game client. A new **exiles** toggle (on by default) restricts
+ the view to a nation's own cards. The toggle is inert when no nation is
+ selected.
- **Web UI Sync flow**: a **Sync** button in the page header opens a
confirmation modal, runs the same fetch + diff the CLI does (with a
spinner during the request), and shows a categorized preview (new /
diff --git a/CONTEXT.md b/CONTEXT.md
index 4bebb0b..f6d9e81 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -56,8 +56,12 @@ the game, it does not police it.
**Exile**:
A cross-faction link on a card: the card belongs to one faction but may be
played in decks of another faction (its exile faction), reflecting the
-game's exile-forces mechanic. Deck import falls back to the exile link when
-a card is not found under its own faction.
+game's exile-forces mechanic. The card keeps its own faction (a Polish
+Exile card is still Poland); the link only grants a second faction it can
+be used by. Deck import falls back to the exile link when a card is not
+found under its own faction. Collection filtering by faction includes a
+faction's Exile cards by default (as the game client does), toggleable in
+the web UI.
**Diff**:
The comparison of card content between the local database and a fresh API
diff --git a/README.md b/README.md
index 9daa38c..b279975 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,13 @@ browsing, sorting, filters, card details, quantity editing, and full
**Sync** and **Export** flows so the browser is the only surface needed to
manage a collection.
+Filtering by nation includes that nation's **exile** cards by default — cards
+that belong to another nation but can be played in its decks (for example the
+Polish *T-34 76 PL* under Soviet), the way the game client shows them. Uncheck
+the **exiles** toggle to restrict the view to a nation's own cards. The toggle
+has no effect when no nation is selected, since every card is already listed
+under its own nation.
+
Click **Edit** to enable quantity changes. Quantity writes are server-side
validated by rarity caps:
diff --git a/kardscm/locales/de.toml b/kardscm/locales/de.toml
index 53e4e90..cd59be2 100644
--- a/kardscm/locales/de.toml
+++ b/kardscm/locales/de.toml
@@ -93,6 +93,7 @@ page_title = "Kards Kollektion"
search_placeholder = "nach Name suchen…"
toggle_spawnable = "Erstellbare"
toggle_reserved = "Reservierte"
+toggle_exiles = "exiles"
toggle_owned = "Nur im Besitz"
col_cost = "Kosten"
card_id_label = "Karten-ID"
diff --git a/kardscm/locales/en.toml b/kardscm/locales/en.toml
index 9624cb4..5644e92 100644
--- a/kardscm/locales/en.toml
+++ b/kardscm/locales/en.toml
@@ -101,6 +101,7 @@ page_title = "kardscm collection"
search_placeholder = "search by name…"
toggle_spawnable = "spawnable"
toggle_reserved = "reserved"
+toggle_exiles = "exiles"
toggle_owned = "only owned"
filter_abilities = "Abilities"
filter_extra_abilities = "Extra abilities"
diff --git a/kardscm/locales/es.toml b/kardscm/locales/es.toml
index 9752d21..275a493 100644
--- a/kardscm/locales/es.toml
+++ b/kardscm/locales/es.toml
@@ -92,6 +92,7 @@ page_title = "colección kardscm"
search_placeholder = "buscar por nombre…"
toggle_spawnable = "invocable"
toggle_reserved = "reservado"
+toggle_exiles = "exiles"
toggle_owned = "solo en posesión"
col_cost = "Costo"
card_id_label = "ID de carta"
diff --git a/kardscm/locales/fr.toml b/kardscm/locales/fr.toml
index 735c48c..604dc71 100644
--- a/kardscm/locales/fr.toml
+++ b/kardscm/locales/fr.toml
@@ -92,6 +92,7 @@ page_title = "kardscm collection"
search_placeholder = "search by name…"
toggle_spawnable = "spawnable"
toggle_reserved = "reserved"
+toggle_exiles = "exiles"
toggle_owned = "only owned"
col_cost = "Cost"
card_id_label = "cardId"
diff --git a/kardscm/locales/it.toml b/kardscm/locales/it.toml
index 0186f00..ecc6e36 100644
--- a/kardscm/locales/it.toml
+++ b/kardscm/locales/it.toml
@@ -92,6 +92,7 @@ page_title = "collezione kardscm"
search_placeholder = "cerca per nome…"
toggle_spawnable = "generabile"
toggle_reserved = "riservato"
+toggle_exiles = "exiles"
toggle_owned = "solo possedute"
col_cost = "Costo"
card_id_label = "ID carta"
diff --git a/kardscm/locales/ja.toml b/kardscm/locales/ja.toml
index 92b2b45..812ff7d 100644
--- a/kardscm/locales/ja.toml
+++ b/kardscm/locales/ja.toml
@@ -93,6 +93,7 @@ page_title = "kardscm collection"
search_placeholder = "search by name…"
toggle_spawnable = "spawnable"
toggle_reserved = "reserved"
+toggle_exiles = "exiles"
toggle_owned = "only owned"
col_cost = "Cost"
card_id_label = "cardId"
diff --git a/kardscm/locales/ko.toml b/kardscm/locales/ko.toml
index 4d4ad38..2938197 100644
--- a/kardscm/locales/ko.toml
+++ b/kardscm/locales/ko.toml
@@ -92,6 +92,7 @@ page_title = "kardscm collection"
search_placeholder = "search by name…"
toggle_spawnable = "spawnable"
toggle_reserved = "reserved"
+toggle_exiles = "exiles"
toggle_owned = "only owned"
col_cost = "Cost"
card_id_label = "cardId"
diff --git a/kardscm/locales/pl.toml b/kardscm/locales/pl.toml
index 00425db..d78daa7 100644
--- a/kardscm/locales/pl.toml
+++ b/kardscm/locales/pl.toml
@@ -92,6 +92,7 @@ page_title = "kolekcja kardscm"
search_placeholder = "szukaj po nazwie…"
toggle_spawnable = "do wystawienia"
toggle_reserved = "zarezerwowane"
+toggle_exiles = "exiles"
toggle_owned = "tylko posiadane"
col_cost = "Koszt"
card_id_label = "cardId"
diff --git a/kardscm/locales/pt.toml b/kardscm/locales/pt.toml
index 5aad38a..5c09eb5 100644
--- a/kardscm/locales/pt.toml
+++ b/kardscm/locales/pt.toml
@@ -92,6 +92,7 @@ page_title = "coleção kardscm"
search_placeholder = "buscar por nome…"
toggle_spawnable = "gerável"
toggle_reserved = "reservado"
+toggle_exiles = "exiles"
toggle_owned = "apenas possuídas"
col_cost = "Custo"
card_id_label = "ID da carta"
diff --git a/kardscm/locales/ru.toml b/kardscm/locales/ru.toml
index 12e8794..0299904 100644
--- a/kardscm/locales/ru.toml
+++ b/kardscm/locales/ru.toml
@@ -92,6 +92,7 @@ page_title = "Коллекция kardscm"
search_placeholder = "поиск по названию…"
toggle_spawnable = "спаунятся"
toggle_reserved = "в резерве"
+toggle_exiles = "в изгнании"
toggle_owned = "только мои"
filter_abilities = "Способности"
filter_extra_abilities = "Доп-способности"
diff --git a/kardscm/locales/zh-Hant.toml b/kardscm/locales/zh-Hant.toml
index 3f7b11f..d435f14 100644
--- a/kardscm/locales/zh-Hant.toml
+++ b/kardscm/locales/zh-Hant.toml
@@ -92,6 +92,7 @@ page_title = "kardscm collection"
search_placeholder = "search by name…"
toggle_spawnable = "spawnable"
toggle_reserved = "reserved"
+toggle_exiles = "exiles"
toggle_owned = "only owned"
col_cost = "Cost"
card_id_label = "cardId"
diff --git a/kardscm/locales/zh.toml b/kardscm/locales/zh.toml
index 94bedb3..f8c4fc5 100644
--- a/kardscm/locales/zh.toml
+++ b/kardscm/locales/zh.toml
@@ -92,6 +92,7 @@ page_title = "kardscm collection"
search_placeholder = "search by name…"
toggle_spawnable = "spawnable"
toggle_reserved = "reserved"
+toggle_exiles = "exiles"
toggle_owned = "only owned"
col_cost = "Cost"
card_id_label = "cardId"
diff --git a/kardscm/web/deps.py b/kardscm/web/deps.py
index dd407d0..722bf25 100644
--- a/kardscm/web/deps.py
+++ b/kardscm/web/deps.py
@@ -40,9 +40,16 @@ def card_filters_dep(
q: str = Query(default=""),
spawnable: bool = Query(default=False),
reserved: bool = Query(default=False),
+ exiles: list[str] = Query(default=[]),
owned: bool = Query(default=False),
) -> CardFilters:
- """FastAPI dependency that builds CardFilters from query string params."""
+ """FastAPI dependency that builds CardFilters from query string params.
+
+ `exiles` arrives as a list because the exile toggle defaults to ON: the
+ template pairs a hidden ``exiles=false`` with the checkbox's ``exiles=true``
+ so an unchecked box still submits a value. Absent entirely (a bare page
+ load with no form) means "use the default", which is ON.
+ """
return CardFilters(
factions=factions,
types=types,
@@ -54,6 +61,7 @@ def card_filters_dep(
text_query=q.strip(),
include_spawnable=spawnable,
include_reserved=reserved,
+ include_exiles=("true" in exiles) if exiles else True,
owned_only=owned,
)
diff --git a/kardscm/web/queries.py b/kardscm/web/queries.py
index a6977b1..eec60ff 100644
--- a/kardscm/web/queries.py
+++ b/kardscm/web/queries.py
@@ -50,6 +50,7 @@ class CardFilters:
text_query: str = ""
include_spawnable: bool = False
include_reserved: bool = False
+ include_exiles: bool = True
owned_only: bool = False
@@ -70,8 +71,17 @@ def _build_where(filters: CardFilters, locale_key: str) -> tuple[list[str], list
if filters.factions:
placeholders = ",".join("?" for _ in filters.factions)
- where.append(f"faction IN ({placeholders})")
- params.extend(filters.factions)
+ if filters.include_exiles:
+ # An exile card keeps its own faction but may be played by its
+ # exile faction, so a nation view surfaces it too — matching the
+ # game client. Without a nation filter this branch is moot: every
+ # card is already listed under its own faction.
+ where.append(f"(faction IN ({placeholders}) OR exile IN ({placeholders}))")
+ params.extend(filters.factions)
+ params.extend(filters.factions)
+ else:
+ where.append(f"faction IN ({placeholders})")
+ params.extend(filters.factions)
if filters.types:
placeholders = ",".join("?" for _ in filters.types)
diff --git a/kardscm/web/templates/_filters.html b/kardscm/web/templates/_filters.html
index e7c887d..efe5f55 100644
--- a/kardscm/web/templates/_filters.html
+++ b/kardscm/web/templates/_filters.html
@@ -104,6 +104,13 @@
{% if filters.include_reserved %}checked{% endif %}>
{{ ui.toggle_reserved }}
+