@property
     def booking_id(self) -> int:
         """If .booking_line, its .booking_id, else -1."""
-        return self.booking_line.booking.id_ if self.booking_line else -1
+        return self.booking.id_ if self.booking else -1
+
+    @property
+    def booking(self) -> Optional['Booking']:
+        """If .booking_line, matching Booking, else None."""
+        return self.booking_line.booking if self.booking_line else None
 
     @property
     def error(self) -> str:
     """Represents lines of individual booking."""
     # pylint: disable=too-few-public-methods
 
-    def __init__(self, id_: int, dat_lines: list[DatLine]) -> None:
-        self.intro_line = IntroLine(self, dat_lines[0].code)
-        dat_lines[0].booking_line = self.intro_line
+    def __init__(self,
+                 id_: int,
+                 dat_lines: list[DatLine],
+                 prev_booking: Optional[Self]
+                 ) -> None:
+        self.intro = IntroLine(self, dat_lines[0].code)
+        dat_lines[0].booking_line = self.intro
         self._transfer_lines = []
         for i, dat_line in enumerate(dat_lines[1:]):
             dat_line.booking_line = TransferLine(self, dat_line.code, i + 1)
             self._transfer_lines[-1].errors += ['needed sink missing']
         self.id_ = id_
         self.dat_lines = dat_lines
+        self.prev = prev_booking
+        if self.prev:
+            self.prev.next = self
+        self.next: Optional[Self] = None
+
+    def can_move(self, up: bool) -> bool:
+        """Whether movement rules would allow self to move up or down."""
+        if (up and ((not self.prev)
+                    or self.prev.intro.date != self.intro.date)):
+            return False
+        if ((not up) and ((not self.next)
+                          or self.next.intro.date != self.intro.date)):
+            return False
+        return True
 
     @property
     def is_questionable(self) -> bool:
         """Whether lines count any errors."""
-        for _ in [bl for bl in [self.intro_line] + self._transfer_lines
+        for _ in [bl for bl in [self.intro] + self._transfer_lines
                   if bl.errors]:
             return True
         return False
             id_ = int(self.path_toks[2])
             if self.pagename.startswith('edit_'):
                 ctx['id'] = id_
-        elif self.pagename.startswith('ledger_'):
-            ctx['max_id'] = self.server.bookings[-1].id_
         if self.pagename == 'balance':
             id_ = int(self.params.first('up_incl') or '-1')
             valid, balance_roots = self.server.balance_roots(id_)
             if dat_line.code:
                 booking_lines += [dat_line]
             elif booking_lines:
-                booking = Booking(len(self.bookings), booking_lines)
-                if last_date > booking.intro_line.date:
-                    booking.intro_line.errors += ['date < previous valid date']
+                booking = Booking(
+                        len(self.bookings), booking_lines,
+                        self.bookings[-1] if self.bookings else None)
+                if last_date > booking.intro.date:
+                    booking.intro.errors += ['date < previous valid date']
                 else:
-                    last_date = booking.intro_line.date
+                    last_date = booking.intro.date
                 self.bookings += [booking]
                 booking_lines = []
 
         if to_end or copied is self.bookings[-1]:
             intro = DatLine(
                 f'{dt_date.today().isoformat()} '
-                f'{copied.intro_line.target} ; {copied.dat_lines[0].comment}')
+                f'{copied.intro.target} ; {copied.dat_lines[0].comment}')
             self.dat_lines += [empty_line, intro] + copied.dat_lines[1:]
             prev_id = self.bookings[-1].id_
         else:
 
 table.ledger tr > td:first-child { background-color: white; }
 {% endmacro %}
 
-{% macro table_dat_lines(dat_lines, max_id, raw) %}
+{% macro table_dat_lines(dat_lines, raw) %}
 <form action="/ledger_{% if raw %}raw{% else %}structured{% endif %}" method="POST">
 <table class="ledger">
 {% for dat_line in dat_lines %}
   {% endif %}
   <tr class="alternating{% if dat_line.is_questionable %} warning{% endif %}">
   {% if dat_line.is_intro %}
-    <td id="{{dat_line.booking_id}}"><a href="#{{dat_line.booking_id}}">[#]</a><input type="submit" name="move_{{dat_line.booking_id}}_up" value="^"{% if dat_line.booking_id == 0 %} disabled{% endif %}/></td>
+    <td id="{{dat_line.booking_id}}"><a href="#{{dat_line.booking_id}}">[#]</a><input type="submit" name="move_{{dat_line.booking_id}}_up" value="^"{% if not dat_line.booking.can_move(1) %} disabled{% endif %}/></td>
   {% elif dat_line.booking_line.idx == 1 %}
-    <td><a href="/balance?up_incl={{dat_line.booking_id}}">[b]</a><input type="submit" name="move_{{dat_line.booking_id}}_down" value="v"{% if dat_line.booking_id == max_id %} disabled{% endif %}/></td>
+    <td><a href="/balance?up_incl={{dat_line.booking_id}}">[b]</a><input type="submit" name="move_{{dat_line.booking_id}}_down" value="v"{% if not dat_line.booking.can_move(0) %} disabled{% endif %}/></td>
   {% elif dat_line.booking_line.idx == 2 %}
     <td><input type="submit" name="copy_{{dat_line.booking_id}}_here" value="c" /><input type="submit" name="copy_{{dat_line.booking_id}}_to_end" value="C" /></td>
   {% else %}