diff --git a/app/templates/cotton/transaction/_transfer_leg.html b/app/templates/cotton/transaction/_transfer_leg.html
new file mode 100644
index 00000000..a3cd8e50
--- /dev/null
+++ b/app/templates/cotton/transaction/_transfer_leg.html
@@ -0,0 +1,50 @@
+{% load i18n %}
+{# One side of a combined transfer, laid out like a mini transaction: account + metadata | amount #}
+{# Only the divider between legs gets the extra space; the outer edges stay flush #}
+
+ {# Timeline rail: x sits under the transfer icon's center (7.5px into the column, minus the container's ps-5 = 20px). #}
+ {# The first leg's rail reaches up past the description's mb + container mt to the icon; the last one stops at its icon, making └ #}
+
+ {# Stub from the rail into this leg's icon, at the icon's vertical center #}
+
+ {# h-5 matches the account line's height so the icon centers on it #}
+
+
+
+ {# Like a regular item: amount drops below on small screens, bottom-right on lg #}
+
+
+
{% if leg.account.group %}{{ leg.account.group.name }} • {% endif %}{{ leg.account.name }}
+ {# Same rhythm as a regular item's metadata rows: mb-2 lg:mb-1 between rows, pe-1 + mr-1 after the icon #}
+
+ {# h-4 = one text-xs line, so each icon centers on its row's first line #}
+ {% if leg.category %}
{{ leg.category.name }}
{% endif %}
+ {% if leg.tags.all %}
{{ leg.tags.all|join:", " }}
{% endif %}
+ {% if leg.entities.all %}
{{ leg.entities.all|join:", " }}
{% endif %}
+
+
+ {# Mobile gaps match a regular item's amount rows (mb-2 lg:mb-0) #}
+
+
+ {% if not dummy %}
+ {% with exchanged=leg.exchanged_amount %}
+ {% if exchanged %}
+
+ {% endif %}
+ {% endwith %}
+ {% endif %}
+
+
+
diff --git a/app/templates/cotton/transaction/item.html b/app/templates/cotton/transaction/item.html
index 6b518db4..831d996a 100644
--- a/app/templates/cotton/transaction/item.html
+++ b/app/templates/cotton/transaction/item.html
@@ -76,42 +76,22 @@
{{ overriden_entities|join:", " }}
- {% elif is_transfer %}
- {% if transaction.entities.all or transfer_to.entities.all %}
-
-
-
{{ transaction.entities.all|join:", "|default:"—" }}
-
-
{{ transfer_to.entities.all|join:", "|default:"—" }}
-
- {% endif %}
-
{% comment %} If no override, fall back to transaction entities, but ONLY if the transaction has an ID. {% endcomment %}
- {% elif transaction.id and transaction.entities.all %}
+ {% elif transaction.id and transaction.entities.all and not is_transfer %}
{{ transaction.entities.all|join:", " }}
{% endif %}
{# Notes#}
- {% if transaction.notes %}
+ {% if transaction.notes and not is_transfer %}
{{ transaction.notes | limited_markdown | linebreaksbr }}
{% endif %}
{# Category#}
- {% if is_transfer %}
- {# Each side keeps its own category: "from" line, then "to" line with the arrow under the icon #}
- {% if transaction.category or transfer_to.category %}
-
-
-
{{ transaction.category.name|default:"—" }}
-
-
{{ transfer_to.category.name|default:"—" }}
-
- {% endif %}
- {% elif transaction.category %}
+ {% if transaction.category and not is_transfer %}
{{ transaction.category.name }}
@@ -125,52 +105,33 @@
{{ overriden_tags|join:", " }}
- {% elif is_transfer %}
- {% if transaction.tags.all or transfer_to.tags.all %}
-
-
-
{{ transaction.tags.all|join:", "|default:"—" }}
-
-
{{ transfer_to.tags.all|join:", "|default:"—" }}
-
- {% endif %}
-
{% comment %} If no override, fall back to transaction tags, but ONLY if the transaction has an ID. {% endcomment %}
- {% elif transaction.id and transaction.tags.all %}
+ {% elif transaction.id and transaction.tags.all and not is_transfer %}
{{ transaction.tags.all|join:", " }}
{% endif %}
+ {% if is_transfer %}
+ {# ps-5 leaves room for the timeline rail that drops from the transfer icon (see _transfer_leg) #}
+
+ {% translate 'From' as from_label %}{% translate 'To' as to_label %}
+ {% include "cotton/transaction/_transfer_leg.html" with leg=transaction color="red" icon="fa-arrow-right-from-bracket" label=from_label first=True %}
+ {% include "cotton/transaction/_transfer_leg.html" with leg=transfer_to color="green" icon="fa-arrow-right-to-bracket" label=to_label first=False %}
+
+ {# Notes go below the legs so they don't sit in the rail's path #}
+ {% if transaction.notes %}
+
+
+
{{ transaction.notes | limited_markdown | linebreaksbr }}
+
+ {% endif %}
+ {% endif %}
+ {% if not is_transfer %}
- {% if is_transfer %}
- {# Each side: amount, with its account below, like a regular item #}
-
-
-
-
-
{% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
-
-
-
-
-
{% if transfer_to.account.group %}{{ transfer_to.account.group.name }} • {% endif %}{{ transfer_to.account.name }}
-
-
- {% else %}
- {% endif %}
{# Exchange Rate#}
{% if not dummy %}
{% with exchanged=transaction.exchanged_amount %}
@@ -195,12 +155,11 @@
{% endif %}
{% endwith %}
{% endif %}
- {% if not is_transfer %}
-
- {% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
-
- {% endif %}
+
+ {% if transaction.account.group %}{{ transaction.account.group.name }} • {% endif %}{{ transaction.account.name }}
+
+ {% endif %}
{% if not dummy %}
{# Item actions#}