The network topology is as simple as it gets: 
After some correspondence my ISP flipped some switch to enable the router to get what the router calls a "Default IPv6 Gateway." My Ethernet-connected PC can now use IPv6 without issue:
$ ping -q -c 1 ipv6.google.com
PING ipv6.google.com(syd15s01-in-x0e.1e100.net (2404:6800:4006:806::200e)) 56 data bytes
--- ipv6.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 45.496/45.496/45.496/0.000 ms
The laptop, running the same software on the same distro (Arch Linux), is able to connect to IPv4 hosts but not IPv6 ones:
$ ping -q -c 1 ipv6.google.com
connect: Network is unreachable
When asking my ISP about this they responded
Do you have IPv6 configured on the wireless adapter in the computer?
How can I tell? The closest I can think if is that the laptop seems to have assigned an internal IP to the network interface:
$ ip address show dev wlp1s0
2: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 54:8c:a0:52:3e:a1 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.6/24 brd 192.168.1.255 scope global noprefixroute wlp1s0
valid_lft forever preferred_lft forever
inet6 fe80::f0ac:8fe:bb0d:619a/64 scope link
valid_lft forever preferred_lft forever
My laptop does not have an Ethernet plug, so I can't check whether it would receive an IPv6 address via Ethernet.
The closest page I can think of in the router configuration is this:
I've been in touch further with my ISP, and they maintain that with the current settings it should just work. I just connected to the wireless network with my desktop PC and can confirm that it does indeed receive a DHCPv6 lease.
Both machines have identical /etc/dhcpcd.conf and /etc/netconfig files (confirmed using diff -u <(ssh laptop cat /etc/…) /etc/…). Even so, when connecting to Wi-Fi the dhcpcd log is very different. On my PC:
$ journalctl --follow --output=cat --unit=dhcpcd | grep ^eno1
eno1: rebinding lease of 192.168.1.2
eno1: leased 192.168.1.2 for 86400 seconds
eno1: adding route to 192.168.1.0/24
eno1: adding default route via 192.168.1.1
eno1: soliciting an IPv6 router
eno1: Router Advertisement from xxxx::xxxx:xxxx:xxxx:xxxx
eno1: adding address xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/64
eno1: adding route to xxxx:xxxx:xxxx:xxxx::/64
eno1: adding default route via xxxx::xxxx:xxxx:xxxx:xxxx
eno1: soliciting a DHCPv6 lease
On the laptop:
$ journalctl --follow --output=cat --unit=dhcpcd | grep ^wlp1s0
wlp1s0: rebinding lease of 192.168.1.6
wlp1s0: leased 192.168.1.6 for 86400 seconds
wlp1s0: adding route to 192.168.1.0/24
wlp1s0: adding default route via 192.168.1.1
wlp1s0: soliciting an IPv6 router
wlp1s0: no IPv6 Routers available
Their Wi-Fi connection configuration is also identical except for the interface name:
Description='Automatically generated profile by wifi-menu'
Interface=wlo1
Connection=wireless
Security=wpa
ESSID=…
IP=dhcp
Key=…
Wireshark reports that the laptop does indeed receive an ICMPv6 router advertisement from the correct MAC address, supporting the idea that the problem is with the laptop not handling the router advertisement.
Diffing the IP tables of the two machines seems to have given a clue: There are some rules left over since a UFW installation years ago on my laptop. Diffing the IP tables of the two machines shows this:
$ comm -23 <(ssh laptop cat iptable.txt) iptable.txt
-P INPUT DROP
-P FORWARD DROP
-N ufw-after-forward
-N ufw-after-input
-N ufw-after-logging-forward
-N ufw-after-logging-input
-N ufw-after-logging-output
-N ufw-after-output
-N ufw-before-forward
-N ufw-before-input
-N ufw-before-logging-forward
-N ufw-before-logging-input
-N ufw-before-logging-output
-N ufw-before-output
-N ufw-logging-allow
-N ufw-logging-deny
-N ufw-not-local
-N ufw-reject-forward
-N ufw-reject-input
-N ufw-reject-output
-N ufw-skip-to-policy-forward
-N ufw-skip-to-policy-input
-N ufw-skip-to-policy-output
-N ufw-track-forward
-N ufw-track-input
-N ufw-track-output
-N ufw-user-forward
-N ufw-user-input
-N ufw-user-limit
-N ufw-user-limit-accept
-N ufw-user-logging-forward
-N ufw-user-logging-input
-N ufw-user-logging-output
-N ufw-user-output
…
I guess the -P INPUT DROP could be responsible for this issue? IPv6 tables show similar differences:
-P INPUT DROP
-P FORWARD DROP
-N ufw6-after-forward
-N ufw6-after-input
-N ufw6-after-logging-forward
-N ufw6-after-logging-input
-N ufw6-after-logging-output
-N ufw6-after-output
-N ufw6-before-forward
-N ufw6-before-input
-N ufw6-before-logging-forward
-N ufw6-before-logging-input
-N ufw6-before-logging-output
-N ufw6-before-output
-N ufw6-logging-allow
-N ufw6-logging-deny
-N ufw6-reject-forward
-N ufw6-reject-input
-N ufw6-reject-output
-N ufw6-skip-to-policy-forward
-N ufw6-skip-to-policy-input
-N ufw6-skip-to-policy-output
-N ufw6-track-forward
-N ufw6-track-input
-N ufw6-track-output
-N ufw6-user-forward
-N ufw6-user-input
-N ufw6-user-limit
-N ufw6-user-limit-accept
-N ufw6-user-logging-forward
-N ufw6-user-logging-input
-N ufw6-user-logging-output
-N ufw6-user-output
